From c72a86272446c0e4fb33c68601563549044b29e6 Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Thu, 28 Nov 2024 18:27:13 +0000 Subject: [PATCH 01/44] Fix build failures with NO_IPV6 defined. In commit 20f818af1201277 I renamed a lot of variables called 'ret', by using clang-rename to do the heavy lifting. But clang-rename only saw instances of the variable name that the _compiler_ saw. The ones that never got through the preprocessor weren't renamed, and I didn't eyeball the patch hard enough to find instances in the #else branch of ifdefs that should also have been renamed. Thanks to Lars Wendler for the report and the fixes. --- unix/network.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/unix/network.c b/unix/network.c index 2ebd76af..00848ab8 100644 --- a/unix/network.c +++ b/unix/network.c @@ -279,7 +279,7 @@ SockAddr *sk_nonamelookup(const char *host) #ifndef NO_IPV6 addr->ais = NULL; #else - ret->addresses = NULL; + addr->addresses = NULL; #endif addr->refcount = 1; return addr; @@ -907,7 +907,7 @@ Socket *sk_newlistener(const char *srcaddr, int port, Plug *plug, u.sin.sin_addr.s_addr = inet_addr(srcaddr); if (u.sin.sin_addr.s_addr != (in_addr_t)(-1)) { /* Override localhost_only with specified listen addr. */ - ret->localhost_only = ipv4_is_loopback(u.sin.sin_addr); + s->localhost_only = ipv4_is_loopback(u.sin.sin_addr); } addr = &u; addrlen = sizeof(u.sin); @@ -1641,8 +1641,8 @@ SockAddr *platform_get_x11_unix_address(const char *sockpath, int displaynum) #ifndef NO_IPV6 addr->ais = NULL; #else - ret->addresses = NULL; - ret->naddresses = 0; + addr->addresses = NULL; + addr->naddresses = 0; #endif addr->refcount = 1; return addr; @@ -1666,8 +1666,8 @@ SockAddr *unix_sock_addr(const char *path) #ifndef NO_IPV6 addr->ais = NULL; #else - ret->addresses = NULL; - ret->naddresses = 0; + addr->addresses = NULL; + addr->naddresses = 0; #endif addr->refcount = 1; return addr; From 8805cf3d9a1bb39c190345b9820ecefa9cfe801d Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Thu, 28 Nov 2024 18:30:48 +0000 Subject: [PATCH 02/44] Fix a build failure with NO_GSSAPI defined. The stub no-gss.c still wanted to know the layout of the ssh_gss_liblist structure, in order to fill it in with nothing. --- ssh/gss.h | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/ssh/gss.h b/ssh/gss.h index c819d48b..d11a359f 100644 --- a/ssh/gss.h +++ b/ssh/gss.h @@ -3,6 +3,13 @@ #include "putty.h" #include "pgssapi.h" +/* This struct is defined even in NO_GSSAPI mode, so that stubs/no-gss.c can + * return an instance of it containing no libraries */ +struct ssh_gss_liblist { + struct ssh_gss_library *libraries; + int nlibraries; +}; + #ifndef NO_GSSAPI #define SSH2_GSS_OIDTYPE 0x06 @@ -49,10 +56,6 @@ struct ssh_gss_library; * The free function cleans up the structure, and its associated * libraries (if any). */ -struct ssh_gss_liblist { - struct ssh_gss_library *libraries; - int nlibraries; -}; struct ssh_gss_liblist *ssh_gss_setup(Conf *conf); void ssh_gss_cleanup(struct ssh_gss_liblist *list); From 948a4c8e234f8600029e57f4d1120f75bd8bdd00 Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Thu, 28 Nov 2024 18:31:43 +0000 Subject: [PATCH 03/44] Fix a compile warning when building with GTK 1. gtkwin_deny_term_resize() is unused in a GTK1 build, triggering an 'unused static function' compiler warning. --- unix/window.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unix/window.c b/unix/window.c index bb772d32..db0eff31 100644 --- a/unix/window.c +++ b/unix/window.c @@ -2546,13 +2546,13 @@ static void gtkwin_set_raw_mouse_mode_pointer(TermWin *tw, bool activate) static void compute_whole_window_size(GtkFrontend *inst, int wchars, int hchars, int *wpix, int *hpix); -#endif static void gtkwin_deny_term_resize(void *vctx) { GtkFrontend *inst = (GtkFrontend *)vctx; drawing_area_setup_simple(inst); } +#endif static void gtkwin_timer(void *vctx, unsigned long now) { From d4e848a962fb34841da6791b2808c652127a1576 Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Thu, 28 Nov 2024 18:36:20 +0000 Subject: [PATCH 04/44] CHECKLST: update for some extra test builds. Those would have caught the two build problems in 0.82 before releasing it. Might as well put them on the list for the future. --- CHECKLST.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHECKLST.txt b/CHECKLST.txt index 92ef67e5..01f57c30 100644 --- a/CHECKLST.txt +++ b/CHECKLST.txt @@ -133,7 +133,9 @@ Making a release candidate build + on at least a reasonably current stable Linux distro, and also try Debian sid + test-build with all of GTK 1, 2 and 3 - + test-build with -DNOT_X_WINDOWS + + test-build with -DNOT_X_WINDOWS (compile flag) + + test-build with -DPUTTY_GSSAPI=OFF (cmake flag) + + test-build with -DPUTTY_IPV6=OFF (cmake flag) * test that the Windows source builds with Visual Studio (just in case there's an unguarded clangism that would prevent it) * quick check of the outlying network protocols (Telnet, SUPDUP From ebe2453446b55fe11b154ace45d95b605b9670ff Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Thu, 28 Nov 2024 21:20:23 +0000 Subject: [PATCH 05/44] psftp: use cmdline_arg_to_filename for batch files. On Windows, this means they can use non-CP_ACP characters. Also it has the side effect of cloning the filename out of the CmdlineArgList, which makes it still valid after cmdline_arg_list_free (oops). --- psftp.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/psftp.c b/psftp.c index 5afd5e54..69044694 100644 --- a/psftp.c +++ b/psftp.c @@ -2393,7 +2393,7 @@ static void do_sftp_cleanup(void) } } -int do_sftp(int mode, int modeflags, const char *batchfile) +int do_sftp(int mode, int modeflags, Filename *batchfile) { FILE *fp; int ret; @@ -2417,9 +2417,9 @@ int do_sftp(int mode, int modeflags, const char *batchfile) break; } } else { - fp = fopen(batchfile, "r"); + fp = f_open(batchfile, "r", false); if (!fp) { - printf("Fatal: unable to open %s\n", batchfile); + printf("Fatal: unable to open %s\n", filename_to_str(batchfile)); return 1; } ret = 0; @@ -2800,7 +2800,7 @@ int psftp_main(CmdlineArgList *arglist) int mode = 0; int modeflags = 0; bool sanitise_stderr = true; - const char *batchfile = NULL; + Filename *batchfile = NULL; sk_init(); @@ -2845,7 +2845,7 @@ int psftp_main(CmdlineArgList *arglist) version(); } else if (strcmp(argstr, "-b") == 0 && nextarg) { mode = 1; - batchfile = cmdline_arg_to_str(nextarg); + batchfile = cmdline_arg_to_filename(nextarg); arglistpos++; } else if (strcmp(argstr, "-bc") == 0) { modeflags = modeflags | 1; From 54f6fefe61ca4967fefb95228681a8f5385fa95b Mon Sep 17 00:00:00 2001 From: Jacob Nevins Date: Sat, 30 Nov 2024 01:53:34 +0000 Subject: [PATCH 06/44] Docs: pscp/plink now need -h/--help to print usage. As of ecfa6b2734. --- doc/plink.but | 6 +++--- doc/pscp.but | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/plink.but b/doc/plink.but index 8d2849b2..4719f057 100644 --- a/doc/plink.but +++ b/doc/plink.but @@ -34,12 +34,12 @@ to include a \c{set} command like the one above. This section describes the basics of how to use Plink for interactive logins and for automated processes. -Once you've got a console window to type into, you can just type -\c{plink} on its own to bring up a usage message. This tells you the +Once you've got a console window to type into, you can type +\c{plink --help} to bring up a usage message. This tells you the version of Plink you're using, and gives you a brief summary of how to use Plink: -\c C:\>plink +\c C:\>plink --help \c Plink: command-line connection utility \c Release 0.82 \c Usage: plink [options] [user@]host [command] diff --git a/doc/pscp.but b/doc/pscp.but index dbc04c85..94a5caf2 100644 --- a/doc/pscp.but +++ b/doc/pscp.but @@ -32,12 +32,12 @@ to include a \c{set} command like the one above. \H{pscp-usage} PSCP Usage -Once you've got a console window to type into, you can just type -\c{pscp} on its own to bring up a usage message. This tells you the +Once you've got a console window to type into, you can type +\c{pscp -h} to bring up a usage message. This tells you the version of PSCP you're using, and gives you a brief summary of how to use PSCP: -\c C:\>pscp +\c C:\>pscp -h \c PuTTY Secure Copy client \c Release 0.82 \c Usage: pscp [options] [user@]host:source target From b97f20d03a3445e5e8f7f4686c5beff78cf85757 Mon Sep 17 00:00:00 2001 From: Jacob Nevins Date: Sat, 30 Nov 2024 09:25:06 +0000 Subject: [PATCH 07/44] release.pl: Adjust pscp/plink transcript updater. For the docs changes made in 54f6fefe61. --- release.pl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/release.pl b/release.pl index f9507a13..b924d11d 100755 --- a/release.pl +++ b/release.pl @@ -43,13 +43,13 @@ &transform("LATEST.VER", sub { s/^\d+\.\d+$/$version/ }); our $transforming = 0; &transform("doc/pscp.but", sub { - if (/^\\c.*>pscp$/) { $transforming = 1; $_ .= $pscp_transcript; } + if (/^\\c.*>pscp -h$/) { $transforming = 1; $_ .= $pscp_transcript; } elsif (!/^\\c/) { $transforming = 0; } elsif ($transforming) { $_=""; } }); $transforming = 0; &transform("doc/plink.but", sub { - if (/^\\c.*>plink$/) { $transforming = 1; $_ .= $plink_transcript; } + if (/^\\c.*>plink --help$/) { $transforming = 1; $_ .= $plink_transcript; } elsif (!/^\\c/) { $transforming = 0; } elsif ($transforming) { $_=""; } }); From 6a88b294276b9c24584efa18b9a37f437fa46712 Mon Sep 17 00:00:00 2001 From: Jacob Nevins Date: Wed, 4 Dec 2024 17:21:30 +0000 Subject: [PATCH 08/44] Unix PuTTY/pterm: fix UB with small keypad. We were relying on uninitialised data. Found by UBSAN. (Introduced in commit c88b6d1853, I think.) --- unix/window.c | 1 + 1 file changed, 1 insertion(+) diff --git a/unix/window.c b/unix/window.c index db0eff31..4a8968d6 100644 --- a/unix/window.c +++ b/unix/window.c @@ -1927,6 +1927,7 @@ gint key_event(GtkWidget *widget, GdkEventKey *event, gpointer data) if (event->state & GDK_CONTROL_MASK) break; + consumed_meta_key = false; end = 1 + format_small_keypad_key( output+1, inst->term, sk_key, event->state & GDK_SHIFT_MASK, event->state & GDK_CONTROL_MASK, From 296b6291d39c0cf118cd3081c3ab86a5889eb4d9 Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Sat, 7 Dec 2024 09:37:15 +0000 Subject: [PATCH 09/44] GTK: fix a crash when clicking Cancel on Change Settings. I only observed this in the GTK1 build, but I don't know for sure it can't happen in other situations, so there's no reason not to be careful. What seems to happen is that when the user clicks Cancel on the Change Settings dialog box, we call gtk_widget_destroy on the window, which emits the "destroy" signal on the window, our handler for which frees the whole dlgparam. But _then_ GTK goes through and cleans up all the sub-widgets of the dialog box, and some of those generate extra events. In particular, destroying a list box is done by first deleting all the list entries - and if one of those is selected, the list box's selection changes, triggering an event which calls our callback that tries to look up the control in the dlgparam we just freed. My simple workaround is to defer actually freeing the dlgparam, via a toplevel callback. Then it's still lying around empty while all those random events are firing. --- unix/dialog.c | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/unix/dialog.c b/unix/dialog.c index 835ad978..fa645b3a 100644 --- a/unix/dialog.c +++ b/unix/dialog.c @@ -3345,9 +3345,18 @@ static void dlgparam_destroy(GtkWidget *widget, gpointer data) sfree(dp->selparams[i]); } sfree(dp->selparams); + dp->selparams = NULL; } #endif - sfree(dp); + /* + * Instead of freeing dp right now, defer it until we return to + * the GTK main loop. Then if any other last-minute GTK events + * happen while the rest of the widgets are being cleaned up, our + * handlers will still be able to try to look things up in dp. + * (They won't find anything - we've just emptied it - but at + * least they won't crash while trying.) + */ + queue_toplevel_callback(sfree, dp); } static void messagebox_handler(dlgcontrol *ctrl, dlgparam *dp, From 7da3449586ea3e6faaa92663d32774e28cf4e2e3 Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Sat, 7 Dec 2024 19:28:44 +0000 Subject: [PATCH 10/44] Fix error message when KEXINIT negotiation fails. By putting the wrong error-type enum value in a ScanKexinitsResult, I accidentally caused nonsense messages of the form Selected key exchange algorithm "foo,bar,baz" does not correspond to any supported algorithm where "foo,bar,baz" is the full comma-separated list sent by the server, so it's not even _an_ algorithm as the message suggests. Now the message is the one it should have been all along: Couldn't agree a key exchange algorithm (available: foo,bar,baz) --- ssh/transport2.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ssh/transport2.c b/ssh/transport2.c index 5dd73cfe..b8e0d1c6 100644 --- a/ssh/transport2.c +++ b/ssh/transport2.c @@ -1166,7 +1166,7 @@ static ScanKexinitsResult ssh2_scan_kexinits( * Otherwise, any match failure _is_ a fatal error. */ ScanKexinitsResult skr = { - .success = false, .error = SKR_UNKNOWN_ID, + .success = false, .error = SKR_NO_AGREEMENT, .kind = kexlist_descr[i], .desc = slists[i], }; return skr; From c2d7ea8e67c462341e16d74e7a0ea42edd514635 Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Wed, 4 Dec 2024 13:02:05 +0100 Subject: [PATCH 11/44] Fix use of aligned_alloc() to be ASan-clean. aligned_alloc() is used by testsc for all its memory allocation, to avoid false-positive timing variations that depend on memory alignment rather than actual secret data. But I'd forgotten that aligned_alloc requires the allocation size to be a multiple of the requested alignment. This showed up when I ran testsc in dry-run mode, and my normal build happened to be using ASan, which complains at the invalid allocation size. But it was theoretically a problem in all builds of testsc. (Though, as far as I'm aware, not practically; and it _only_ affected testsc.) --- utils/memory.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/utils/memory.c b/utils/memory.c index 0ba791ad..590be002 100644 --- a/utils/memory.c +++ b/utils/memory.c @@ -35,7 +35,10 @@ void *safemalloc(size_t factor1, size_t factor2, size_t addend) #ifdef MINEFIELD p = minefield_c_malloc(size); #elif defined ALLOCATION_ALIGNMENT - p = aligned_alloc(ALLOCATION_ALIGNMENT, size); + /* aligned_alloc requires the allocation size to be rounded up */ + p = aligned_alloc( + ALLOCATION_ALIGNMENT, + (size + ALLOCATION_ALIGNMENT - 1) & ~(ALLOCATION_ALIGNMENT-1)); #else p = malloc(size); #endif From fcdc804b4f4cee6c5b6a1b436fbc6ec7db06ea3e Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Sun, 1 Dec 2024 09:55:39 +0000 Subject: [PATCH 12/44] Move some NTRU helper routines into a header file. I'm going to want to use these again for ML-KEM, so let's put one copy of them where both algorithms can use it. --- crypto/ntru.c | 56 +++++--------------------------------------- crypto/smallmoduli.h | 54 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 50 deletions(-) create mode 100644 crypto/smallmoduli.h diff --git a/crypto/ntru.c b/crypto/ntru.c index 60c37e2e..a7e53122 100644 --- a/crypto/ntru.c +++ b/crypto/ntru.c @@ -79,58 +79,14 @@ #include "ssh.h" #include "mpint.h" #include "ntru.h" - -/* ---------------------------------------------------------------------- - * Preliminaries: we're going to need to do modular arithmetic on - * small values (considerably smaller than 2^16), and we need to do it - * without using integer division which might not be time-safe. - * - * The strategy for this is the same as I used in - * mp_mod_known_integer: see there for the proofs. The basic idea is - * that we precompute the reciprocal of our modulus as a fixed-point - * number, and use that to get an approximate quotient which we - * subtract off. For these integer sizes, precomputing a fixed-point - * reciprocal of the form (2^48 / modulus) leaves us at most off by 1 - * in the quotient, so there's a single (time-safe) trial subtraction - * at the end. - * - * (It's possible that some speed could be gained by not reducing - * fully at every step. But then you'd have to carefully identify all - * the places in the algorithm where things are compared to zero. This - * was the easiest way to get it all working in the first place.) - */ - -/* Precompute the reciprocal */ -static uint64_t reciprocal_for_reduction(uint16_t q) -{ - return ((uint64_t)1 << 48) / q; -} - -/* Reduce x mod q, assuming qrecip == reciprocal_for_reduction(q) */ -static uint16_t reduce(uint32_t x, uint16_t q, uint64_t qrecip) -{ - uint64_t unshifted_quot = x * qrecip; - uint64_t quot = unshifted_quot >> 48; - uint16_t reduced = x - quot * q; - reduced -= q * (1 & ((q-1 - reduced) >> 15)); - return reduced; -} - -/* Reduce x mod q as above, but also return the quotient */ -static uint16_t reduce_with_quot(uint32_t x, uint32_t *quot_out, - uint16_t q, uint64_t qrecip) -{ - uint64_t unshifted_quot = x * qrecip; - uint64_t quot = unshifted_quot >> 48; - uint16_t reduced = x - quot * q; - uint64_t extraquot = (1 & ((q-1 - reduced) >> 15)); - reduced -= extraquot * q; - *quot_out = quot + extraquot; - return reduced; -} +#include "smallmoduli.h" /* Invert x mod q, assuming it's nonzero. (For time-safety, no check - * is made for zero; it just returns 0.) */ + * is made for zero; it just returns 0.) + * + * Expects qrecip == reciprocal_for_reduction(q). (But it's passed in + * as a parameter to save recomputing it, on the theory that the + * caller will have had it lying around already in most cases.) */ static uint16_t invert(uint16_t x, uint16_t q, uint64_t qrecip) { /* Fermat inversion: compute x^(q-2), since x^(q-1) == 1. */ diff --git a/crypto/smallmoduli.h b/crypto/smallmoduli.h new file mode 100644 index 00000000..b452b410 --- /dev/null +++ b/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; +} From f08da2b638256d73936122d638b177752d6d9838 Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Sat, 7 Dec 2024 19:21:57 +0000 Subject: [PATCH 13/44] Separate NTRU Prime from the hybridisation layer. Now ntru.c contains just the NTRU business, and kex-hybrid.c contains the system for running a post-quantum and a classical KEX and hashing together the results. In between them is a new small vtable API for the key encapsulation mechanisms that the post-quantum standardisation effort seems to be settling on. --- crypto/CMakeLists.txt | 1 + crypto/kex-hybrid.c | 322 +++++++++++++++++++++++++++++ crypto/ntru.c | 458 +++++++++--------------------------------- defs.h | 2 + ssh.h | 47 +++++ 5 files changed, 467 insertions(+), 363 deletions(-) create mode 100644 crypto/kex-hybrid.c diff --git a/crypto/CMakeLists.txt b/crypto/CMakeLists.txt index edb02ce4..0266b2d1 100644 --- a/crypto/CMakeLists.txt +++ b/crypto/CMakeLists.txt @@ -20,6 +20,7 @@ add_sources_from_current_dir(crypto ecc-ssh.c hash_simple.c hmac.c + kex-hybrid.c mac.c mac_simple.c md5.c diff --git a/crypto/kex-hybrid.c b/crypto/kex-hybrid.c new file mode 100644 index 00000000..38390bd0 --- /dev/null +++ b/crypto/kex-hybrid.c @@ -0,0 +1,322 @@ +/* + * 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 +#include +#include + +#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 + 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, +}; + +/* ---------------------------------------------------------------------- + * 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, +}; + +/* ---------------------------------------------------------------------- + * 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, +}; + +/* ---------------------------------------------------------------------- + * 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 = "sntrup761x25519-sha512@openssh.com", + .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, +}; diff --git a/crypto/ntru.c b/crypto/ntru.c index a7e53122..80c35179 100644 --- a/crypto/ntru.c +++ b/crypto/ntru.c @@ -1432,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. */ /* @@ -1451,399 +1444,138 @@ static void ntru_session_hash( #define q_LIVE 4591 #define w_LIVE 286 -static char *ssh_ntru_description(const ssh_kex *kex) -{ - return dupprintf("NTRU Prime / Curve25519 hybrid key exchange"); -} - -/* - * State structure for the client, which takes the role of inventing a - * key pair and decrypting a secret plaintext sent to it by the server. - */ -typedef struct ntru_client_key { +struct ntru_dk { NTRUKeyPair *keypair; - ecdh_key *curve25519; - - ecdh_key ek; -} ntru_client_key; - -static void ssh_ntru_client_free(ecdh_key *dh); -static void ssh_ntru_client_getpublic(ecdh_key *dh, BinarySink *bs); -static bool ssh_ntru_client_getkey(ecdh_key *dh, ptrlen remoteKey, - BinarySink *bs); - -static const ecdh_keyalg ssh_ntru_client_vt = { - /* This vtable has no 'new' method, because it's constructed via - * the selector vt below */ - .free = ssh_ntru_client_free, - .getpublic = ssh_ntru_client_getpublic, - .getkey = ssh_ntru_client_getkey, - .description = ssh_ntru_description, + strbuf *encoded; + pq_kem_dk dk; }; -static ecdh_key *ssh_ntru_client_new(void) +static pq_kem_dk *ntru_vt_keygen(const pq_kemalg *alg, BinarySink *ek) { - 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) -{ - ntru_client_key *nk = container_of(dh, ntru_client_key, ek); - ntru_keypair_free(nk->keypair); - ecdh_key_free(nk->curve25519); - sfree(nk); -} - -static void ssh_ntru_client_getpublic(ecdh_key *dh, BinarySink *bs) -{ - 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); + 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 bool ssh_ntru_client_getkey(ecdh_key *dh, ptrlen remoteKey, - 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); - - /* - * We expect the server to have sent us a string containing a - * ciphertext, a confirmation hash, and a Curve25519 public point. - * Extract all three. - */ BinarySource src[1]; - BinarySource_BARE_INIT_PL(src, remoteKey); + BinarySource_BARE_INIT_PL(src, ek); - uint16_t *ciphertext = snewn(p_LIVE, uint16_t); - ptrlen ciphertext_encoded = ntru_decode_ciphertext( - ciphertext, nk->keypair, src); - ptrlen confirmation_hash = get_data(src, 32); - ptrlen curve25519_remoteKey = get_data(src, 32); + uint16_t *pubkey = snewn(p_LIVE, uint16_t); + ntru_decode_pubkey(pubkey, p_LIVE, q_LIVE, src); if (get_err(src) || get_avail(src)) { - /* Hard-fail if the input wasn't exactly the right length */ - ring_free(ciphertext, p_LIVE); + /* Hard-fail if the input wasn't exactly the right length */ + ring_free(pubkey, p_LIVE); return false; } - /* - * Main hash object which will combine the NTRU and Curve25519 - * outputs. - */ - ssh_hash *h = ssh_hash_new(&ssh_sha512); - - /* Reusable buffer for storing various hash outputs. */ - uint8_t hashdata[64]; - - /* - * NTRU side. - */ - { - /* Decrypt the ciphertext to recover the server's plaintext */ - uint16_t *plaintext = snewn(p_LIVE, uint16_t); - ntru_decrypt(plaintext, ciphertext, nk->keypair); - - /* Make the confirmation hash */ - ntru_confirmation_hash(hashdata, plaintext, nk->keypair->h, - p_LIVE, q_LIVE); - - /* Check it matches the one the server sent */ - unsigned ok = smemeq(hashdata, confirmation_hash.ptr, 32); + /* Invent a valid NTRU plaintext. */ + uint16_t *plaintext = snewn(p_LIVE, uint16_t); + ntru_gen_short(plaintext, p_LIVE, w_LIVE); - /* 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)); - - mp_free(curvekey); - strbuf_free(otherkey); - } - - /* - * Finish up: compute the final output hash (full 64 bytes of - * SHA-512 this time), and return it encoded as a string. - */ - ssh_hash_final(h, hashdata); - put_stringpl(bs, make_ptrlen(hashdata, sizeof(hashdata))); - smemclr(hashdata, sizeof(hashdata)); + /* Encrypt the plaintext, and encode the ciphertext into a strbuf, + * so we can reuse it for both the session hash and sending to the + * client. */ + uint16_t *ciphertext = snewn(p_LIVE, uint16_t); + ntru_encrypt(ciphertext, plaintext, pubkey, p_LIVE, q_LIVE); + strbuf *ciphertext_encoded = strbuf_new_nm(); + ntru_encode_ciphertext(ciphertext, p_LIVE, q_LIVE, + BinarySink_UPCAST(ciphertext_encoded)); + put_datapl(c, ptrlen_from_strbuf(ciphertext_encoded)); + + /* Compute the confirmation hash, and append that to the data sent + * to the other side. */ + uint8_t confhash[32]; + ntru_confirmation_hash(confhash, plaintext, pubkey, p_LIVE, q_LIVE); + put_data(c, confhash, 32); + + /* Compute the session hash, i.e. the output shared secret. */ + uint8_t sesshash[32]; + ntru_session_hash(sesshash, 1, plaintext, p_LIVE, + ptrlen_from_strbuf(ciphertext_encoded), + make_ptrlen(confhash, 32)); + put_data(k, sesshash, 32); + + ring_free(pubkey, p_LIVE); + ring_free(plaintext, p_LIVE); + ring_free(ciphertext, p_LIVE); + strbuf_free(ciphertext_encoded); + smemclr(confhash, sizeof(confhash)); + smemclr(sesshash, sizeof(sesshash)); return true; } -/* - * State structure for the server, which takes the role of inventing a - * secret plaintext and sending it to the client encrypted with the - * public key the client sent. - */ -typedef struct ntru_server_key { - uint16_t *plaintext; - strbuf *ciphertext_encoded, *confirmation_hash; - ecdh_key *curve25519; - - ecdh_key ek; -} ntru_server_key; - -static void ssh_ntru_server_free(ecdh_key *dh); -static void ssh_ntru_server_getpublic(ecdh_key *dh, BinarySink *bs); -static bool ssh_ntru_server_getkey(ecdh_key *dh, ptrlen remoteKey, - BinarySink *bs); - -static const ecdh_keyalg ssh_ntru_server_vt = { - /* This vtable has no 'new' method, because it's constructed via - * the selector vt below */ - .free = ssh_ntru_server_free, - .getpublic = ssh_ntru_server_getpublic, - .getkey = ssh_ntru_server_getkey, - .description = ssh_ntru_description, -}; - -static ecdh_key *ssh_ntru_server_new(void) -{ - ntru_server_key *nk = snew(ntru_server_key); - nk->ek.vt = &ssh_ntru_server_vt; - - nk->plaintext = snewn(p_LIVE, uint16_t); - nk->ciphertext_encoded = strbuf_new_nm(); - nk->confirmation_hash = strbuf_new_nm(); - ntru_gen_short(nk->plaintext, p_LIVE, w_LIVE); - - nk->curve25519 = ecdh_key_new(&ssh_ec_kex_curve25519, false); - - return &nk->ek; -} - -static void ssh_ntru_server_free(ecdh_key *dh) -{ - 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); -} - -static bool ssh_ntru_server_getkey(ecdh_key *dh, ptrlen remoteKey, - BinarySink *bs) +static bool ntru_vt_decaps(pq_kem_dk *dk, BinarySink *k, ptrlen c) { - ntru_server_key *nk = container_of(dh, ntru_server_key, ek); + struct ntru_dk *ndk = container_of(dk, struct ntru_dk, dk); - /* - * In the server, getkey is called first, with the public - * information received from the client. We expect the client to - * have sent us a string containing a public key and a Curve25519 - * public point. - */ + /* Expect a string containing a ciphertext and a confirmation hash. */ BinarySource src[1]; - BinarySource_BARE_INIT_PL(src, remoteKey); + BinarySource_BARE_INIT_PL(src, c); - uint16_t *pubkey = snewn(p_LIVE, uint16_t); - ntru_decode_pubkey(pubkey, p_LIVE, q_LIVE, src); - ptrlen curve25519_remoteKey = get_data(src, 32); + uint16_t *ciphertext = snewn(p_LIVE, uint16_t); + ptrlen ciphertext_encoded = ntru_decode_ciphertext( + ciphertext, ndk->keypair, src); + ptrlen confirmation_hash = get_data(src, 32); if (get_err(src) || get_avail(src)) { - /* Hard-fail if the input wasn't exactly the right length */ - ring_free(pubkey, p_LIVE); + /* Hard-fail if the input wasn't exactly the right length */ + ring_free(ciphertext, p_LIVE); return false; } - /* - * Main hash object which will combine the NTRU and Curve25519 - * outputs. - */ - ssh_hash *h = ssh_hash_new(&ssh_sha512); + /* Decrypt the ciphertext to recover the sender's plaintext */ + uint16_t *plaintext = snewn(p_LIVE, uint16_t); + ntru_decrypt(plaintext, ciphertext, ndk->keypair); - /* Reusable buffer for storing various hash outputs. */ - uint8_t hashdata[64]; + /* Make the confirmation hash */ + uint8_t confhash[32]; + ntru_confirmation_hash(confhash, plaintext, ndk->keypair->h, + p_LIVE, q_LIVE); - /* - * 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); + /* Check it matches the one the server sent */ + unsigned ok = smemeq(confhash, confirmation_hash.ptr, 32); - /* 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); + /* 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]); - /* 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)); + /* 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); - /* And put the NTRU session hash into the main hash object. */ - put_data(h, hashdata, 32); - - /* Now we can free the public key */ - ring_free(pubkey, p_LIVE); - } - - /* - * Curve25519 side. - */ - { - strbuf *otherkey = strbuf_new_nm(); - - /* Call out to Curve25519 to compute the shared secret from that - * kex method */ - bool ok = ecdh_key_getkey(nk->curve25519, curve25519_remoteKey, - BinarySink_UPCAST(otherkey)); - /* As on the client side, abort if Curve25519 reported failure */ - if (!ok) { - ssh_hash_free(h); - smemclr(hashdata, sizeof(hashdata)); - strbuf_free(otherkey); - return false; - } - - /* As on the client side, decode Curve25519's mpint so we can - * re-encode it appropriately for our hash preimage */ - BinarySource src[1]; - BinarySource_BARE_INIT_PL(src, ptrlen_from_strbuf(otherkey)); - mp_int *curvekey = get_mp_ssh2(src); - - for (unsigned i = 32; i-- > 0 ;) - put_byte(h, mp_get_byte(curvekey, i)); - - mp_free(curvekey); - strbuf_free(otherkey); - } - - /* - * Finish up: compute the final output hash (full 64 bytes of - * SHA-512 this time), and return it encoded as a string. - */ - ssh_hash_final(h, hashdata); - put_stringpl(bs, make_ptrlen(hashdata, sizeof(hashdata))); - smemclr(hashdata, sizeof(hashdata)); + ring_free(plaintext, p_LIVE); + ring_free(ciphertext, p_LIVE); + smemclr(confhash, sizeof(confhash)); + smemclr(sesshash, sizeof(sesshash)); return true; } -static void ssh_ntru_server_getpublic(ecdh_key *dh, BinarySink *bs) +static void ntru_vt_free_dk(pq_kem_dk *dk) { - 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) -{ - 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, +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, }; - -static const ssh_kex ssh_ntru_curve25519_openssh = { - .name = "sntrup761x25519-sha512@openssh.com", - .main_type = KEXTYPE_ECDH, - .hash = &ssh_sha512, - .ecdh_vt = &ssh_ntru_selector_vt, -}; - -static const ssh_kex ssh_ntru_curve25519 = { - /* Same as sntrup761x25519-sha512@openssh.com 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 ssh_kexes ssh_ntru_hybrid_kex = { lenof(hybrid_list), hybrid_list }; diff --git a/defs.h b/defs.h index d8bfe02a..286436e9 100644 --- a/defs.h +++ b/defs.h @@ -187,6 +187,8 @@ typedef struct ssh2_ciphers ssh2_ciphers; typedef struct dh_ctx dh_ctx; typedef struct ecdh_key ecdh_key; typedef struct ecdh_keyalg ecdh_keyalg; +typedef struct pq_kemalg pq_kemalg; +typedef struct pq_kem_dk pq_kem_dk; typedef struct NTRUKeyPair NTRUKeyPair; typedef struct NTRUEncodeSchedule NTRUEncodeSchedule; typedef struct RFC6979 RFC6979; diff --git a/ssh.h b/ssh.h index 46356f0c..c3ebcc68 100644 --- a/ssh.h +++ b/ssh.h @@ -1005,6 +1005,52 @@ static inline bool ecdh_key_getkey(ecdh_key *key, ptrlen remoteKey, static inline char *ecdh_keyalg_description(const ssh_kex *kex) { return kex->ecdh_vt->description(kex); } +/* + * vtable for post-quantum key encapsulation methods (things like NTRU + * and ML-KEM). + * + * These work in an asymmetric way that's conceptually more like the + * old RSA kex (either SSH-1 or SSH-2) than like Diffie-Hellman. One + * party generates a keypair and sends the public key; the other party + * invents a secret and encrypts it with the public key; the first + * party receives the ciphertext and decrypts it, and now both parties + * have the secret. + */ +struct pq_kem_dk { + const pq_kemalg *vt; +}; +struct pq_kemalg { + /* Generate a key pair, writing the public encryption key in wire + * format to ek. Return the decryption key. */ + pq_kem_dk *(*keygen)(const pq_kemalg *alg, BinarySink *ek); + /* Invent and encrypt a secret, writing the ciphertext in wire + * format to c and the secret itself to k. Returns false on any + * kind of really obvious validation failure of the encryption key. */ + bool (*encaps)(const pq_kemalg *alg, BinarySink *c, BinarySink *k, + ptrlen ek); + /* Decrypt the secret and write it to k. Returns false on + * validation failure. However, more competent cryptographic + * attacks are rejected in a way that's not obvious, returning a + * useless pseudorandom secret. */ + bool (*decaps)(pq_kem_dk *dk, BinarySink *k, ptrlen c); + /* Free a decryption key. */ + void (*free_dk)(pq_kem_dk *dk); + + const void *extra; + const char *description; + size_t ek_len, c_len; +}; + +static inline pq_kem_dk *pq_kem_keygen(const pq_kemalg *alg, BinarySink *ek) +{ return alg->keygen(alg, ek); } +static inline bool pq_kem_encaps(const pq_kemalg *alg, BinarySink *c, + BinarySink *k, ptrlen ek) +{ return alg->encaps(alg, c, k, ek); } +static inline bool pq_kem_decaps(pq_kem_dk *dk, BinarySink *k, ptrlen c) +{ return dk->vt->decaps(dk, k, c); } +static inline void pq_kem_free_dk(pq_kem_dk *dk) +{ dk->vt->free_dk(dk); } + /* * Suffix on GSSAPI SSH protocol identifiers that indicates Kerberos 5 * as the mechanism. @@ -1194,6 +1240,7 @@ extern const ssh_kex ssh_ec_kex_nistp384; extern const ssh_kex ssh_ec_kex_nistp521; extern const ssh_kexes ssh_ecdh_kex; extern const ssh_kexes ssh_ntru_hybrid_kex; +extern const pq_kemalg ssh_ntru; extern const ssh_keyalg ssh_dsa; extern const ssh_keyalg ssh_rsa; extern const ssh_keyalg ssh_rsa_sha256; From 16629d3bbcae48a84780adbce9fc1a2995889f02 Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Sat, 7 Dec 2024 19:32:46 +0000 Subject: [PATCH 14/44] Add more variants of SHAKE. This adds a ssh_hashalg defining SHAKE256 with a 32-byte output, in addition to the 114-byte output we already have. Also, it defines a new API for using SHAKE128 and SHAKE256 in the more general form of an extendable output function, which is to say that you still have to put in all the input before reading any output, but once you start reading output you can just keep going until you have enough. Both of these will be needed in an upcoming commit implementing ML-KEM. --- crypto/sha3.c | 74 ++++++++++++++++++++++++++++++++++++++++++- defs.h | 1 + ssh.h | 1 + test/cryptsuite.py | 16 ++++++++++ test/testcrypt-func.h | 4 +++ test/testcrypt.c | 9 ++++++ 6 files changed, 104 insertions(+), 1 deletion(-) diff --git a/crypto/sha3.c b/crypto/sha3.c index 83d136bf..efd8f894 100644 --- a/crypto/sha3.c +++ b/crypto/sha3.c @@ -326,4 +326,76 @@ static void shake256_reset(ssh_hash *hash) HASHALG_NAMES_BARE("SHAKE" #param), \ } -DEFINE_SHAKE(256, 114); +DEFINE_SHAKE(256, 114); /* used by Ed448 */ +DEFINE_SHAKE(256, 32); /* used by ML-KEM */ + +struct ShakeXOF { + keccak_state state; + unsigned char *buf; + size_t bytes_per_transform, pos; +}; + +static ShakeXOF *shake_xof_from_input(unsigned bits, ptrlen data) +{ + ShakeXOF *sx = snew_plus(ShakeXOF, 200 * 64); + sx->buf = snew_plus_get_aux(sx); + + /* Initialise as if we were generating 0 bytes of hash. That way, + * keccak_output will do the final accumulation but generate no data. */ + keccak_shake_init(&sx->state, bits, 0); + keccak_accumulate(&sx->state, data.ptr, data.len); + keccak_output(&sx->state, NULL); + + sx->bytes_per_transform = 200 - bits/4; + sx->pos = 0; + + return sx; +} + +ShakeXOF *shake128_xof_from_input(ptrlen data) +{ + return shake_xof_from_input(128, data); +} + +ShakeXOF *shake256_xof_from_input(ptrlen data) +{ + return shake_xof_from_input(256, data); +} + +void shake_xof_read(ShakeXOF *sx, void *output_v, size_t size) +{ + unsigned char *output = (unsigned char *)output_v; + + while (size > 0) { + if (sx->pos == 0) { + /* Copy the 64-bit words from the Keccak state into the + * output buffer of bytes */ + for (unsigned y = 0; y < 5; y++) + for (unsigned x = 0; x < 5; x++) + PUT_64BIT_LSB_FIRST(sx->buf + 8 * (5*y+x), + sx->state.A[x][y]); + } + + /* Read a chunk from the byte buffer */ + size_t this_size = sx->bytes_per_transform - sx->pos; + if (this_size > size) + this_size = size; + memcpy(output, sx->buf + sx->pos, this_size); + sx->pos += this_size; + output += this_size; + size -= this_size; + + /* Retransform the Keccak state if we've run out of data */ + if (sx->pos >= sx->bytes_per_transform) { + keccak_transform(sx->state.A); + sx->pos = 0; + } + } +} + +void shake_xof_free(ShakeXOF *sx) +{ + smemclr(sx->buf, 200 * 64); + smemclr(sx, sizeof(*sx)); + sfree(sx); +} diff --git a/defs.h b/defs.h index 286436e9..0ac23ee2 100644 --- a/defs.h +++ b/defs.h @@ -193,6 +193,7 @@ typedef struct NTRUKeyPair NTRUKeyPair; typedef struct NTRUEncodeSchedule NTRUEncodeSchedule; typedef struct RFC6979 RFC6979; typedef struct RFC6979Result RFC6979Result; +typedef struct ShakeXOF ShakeXOF; typedef struct dlgparam dlgparam; typedef struct dlgcontrol dlgcontrol; diff --git a/ssh.h b/ssh.h index c3ebcc68..8b1a62f0 100644 --- a/ssh.h +++ b/ssh.h @@ -1213,6 +1213,7 @@ extern const ssh_hashalg ssh_sha3_224; extern const ssh_hashalg ssh_sha3_256; extern const ssh_hashalg ssh_sha3_384; extern const ssh_hashalg ssh_sha3_512; +extern const ssh_hashalg ssh_shake256_32bytes; extern const ssh_hashalg ssh_shake256_114bytes; extern const ssh_hashalg ssh_blake2b; extern const ssh_kexes ssh_diffiehellman_group1; diff --git a/test/cryptsuite.py b/test/cryptsuite.py index 5cdba58e..5be86193 100755 --- a/test/cryptsuite.py +++ b/test/cryptsuite.py @@ -3853,6 +3853,22 @@ def testSHA3(self): self.assertEqualBin(hash_str('shake256_114bytes', ''), unhex("46b9dd2b0ba88d13233b3feb743eeb243fcd52ea62b81b82b50c27646ed5762fd75dc4ddd8c0f200cb05019d67b592f6fc821c49479ab48640292eacb3b7c4be141e96616fb13957692cc7edd0b45ae3dc07223c8e92937bef84bc0eab862853349ec75546f58fb7c2775c38462c5010d846")) self.assertEqualBin(hash_str('shake256_114bytes', unhex('a3')*200), unhex("cd8a920ed141aa0407a22d59288652e9d9f1a7ee0c1e7c1ca699424da84a904d2d700caae7396ece96604440577da4f3aa22aeb8857f961c4cd8e06f0ae6610b1048a7f64e1074cd629e85ad7566048efc4fb500b486a3309a8f26724c0ed628001a1099422468de726f1061d99eb9e93604")) + def testSHA3XOF(self): + # Cherry-picked examples from CAVS 19.0, testing both SHAKE128 + # and SHAKE256, each with a long input and a long output. + + xof = shake128_xof_from_input(unhex('a6fe00064257aa318b621c5eb311d32bb8004c2fa1a969d205d71762cc5d2e633907992629d1b69d9557ff6d5e8deb454ab00f6e497c89a4fea09e257a6fa2074bd818ceb5981b3e3faefd6e720f2d1edd9c5e4a5c51e5009abf636ed5bca53fe159c8287014a1bd904f5c8a7501625f79ac81eb618f478ce21cae6664acffb30572f059e1ad0fc2912264e8f1ca52af26c8bf78e09d75f3dd9fc734afa8770abe0bd78c90cc2ff448105fb16dd2c5b7edd8611a62e537db9331f5023e16d6ec150cc6e706d7c7fcbfff930c7281831fd5c4aff86ece57ed0db882f59a5fe403105d0592ca38a081fed84922873f538ee774f13b8cc09bd0521db4374aec69f4bae6dcb66455822c0b84c91a3474ffac2ad06f0a4423cd2c6a49d4f0d6242d6a1890937b5d9835a5f0ea5b1d01884d22a6c1718e1f60b3ab5e232947c76ef70b344171083c688093b5f1475377e3069863')) + self.assertEqualBin(shake_xof_read(xof, 128//8), unhex("3109d9472ca436e805c6b3db2251a9bc")) + + xof = shake128_xof_from_input(unhex('0a13ad2c7a239b4ba73ea6592ae84ea9')) + self.assertEqualBin(shake_xof_read(xof, 1120//8), unhex("5feaf99c15f48851943ff9baa6e5055d8377f0dd347aa4dbece51ad3a6d9ce0c01aee9fe2260b80a4673a909b532adcdd1e421c32d6460535b5fe392a58d2634979a5a104d6c470aa3306c400b061db91c463b2848297bca2bc26d1864ba49d7ff949ebca50fbf79a5e63716dc82b600bd52ca7437ed774d169f6bf02e46487956fba2230f34cd2a0485484d")) + + xof = shake256_xof_from_input(unhex('dc5a100fa16df1583c79722a0d72833d3bf22c109b8889dbd35213c6bfce205813edae3242695cfd9f59b9a1c203c1b72ef1a5423147cb990b5316a85266675894e2644c3f9578cebe451a09e58c53788fe77a9e850943f8a275f830354b0593a762bac55e984db3e0661eca3cb83f67a6fb348e6177f7dee2df40c4322602f094953905681be3954fe44c4c902c8f6bba565a788b38f13411ba76ce0f9f6756a2a2687424c5435a51e62df7a8934b6e141f74c6ccf539e3782d22b5955d3baf1ab2cf7b5c3f74ec2f9447344e937957fd7f0bdfec56d5d25f61cde18c0986e244ecf780d6307e313117256948d4230ebb9ea62bb302cfe80d7dfebabc4a51d7687967ed5b416a139e974c005fff507a96')) + self.assertEqualBin(shake_xof_read(xof, 256//8), unhex("2bac5716803a9cda8f9e84365ab0a681327b5ba34fdedfb1c12e6e807f45284b")) + + xof = shake256_xof_from_input(unhex('8d8001e2c096f1b88e7c9224a086efd4797fbf74a8033a2d422a2b6b8f6747e4')) + self.assertEqualBin(shake_xof_read(xof, 2000//8), unhex("2e975f6a8a14f0704d51b13667d8195c219f71e6345696c49fa4b9d08e9225d3d39393425152c97e71dd24601c11abcfa0f12f53c680bd3ae757b8134a9c10d429615869217fdd5885c4db174985703a6d6de94a667eac3023443a8337ae1bc601b76d7d38ec3c34463105f0d3949d78e562a039e4469548b609395de5a4fd43c46ca9fd6ee29ada5efc07d84d553249450dab4a49c483ded250c9338f85cd937ae66bb436f3b4026e859fda1ca571432f3bfc09e7c03ca4d183b741111ca0483d0edabc03feb23b17ee48e844ba2408d9dcfd0139d2e8c7310125aee801c61ab7900d1efc47c078281766f361c5e6111346235e1dc38325666c")) + def testBLAKE2b(self): # Test case from RFC 7693 appendix A. self.assertEqualBin(hash_str('blake2b', b'abc'), unhex( diff --git a/test/testcrypt-func.h b/test/testcrypt-func.h index 05121458..a39f5788 100644 --- a/test/testcrypt-func.h +++ b/test/testcrypt-func.h @@ -264,6 +264,10 @@ FUNC(void, ssh_hash_update, ARG(val_hash, h), ARG(val_string_ptrlen, data)) FUNC(opt_val_hash, blake2b_new_general, ARG(uint, hashlen)) +FUNC(val_shakexof, shake128_xof_from_input, ARG(val_string_ptrlen, input)) +FUNC(val_shakexof, shake256_xof_from_input, ARG(val_string_ptrlen, input)) +FUNC_WRAPPED(val_string, shake_xof_read, ARG(val_shakexof, xof), ARG(uint, size)) + /* * The ssh2_mac abstraction. Note the optional ssh_cipher parameter * to ssh2_mac_new. Also, again, I've invented an ssh2_mac_update so diff --git a/test/testcrypt.c b/test/testcrypt.c index 3b495bba..b1466dbc 100644 --- a/test/testcrypt.c +++ b/test/testcrypt.c @@ -99,6 +99,7 @@ uint64_t prng_reseed_time_ms(void) X(millerrabin, MillerRabin *, miller_rabin_free(v)) \ X(ntrukeypair, NTRUKeyPair *, ntru_keypair_free(v)) \ X(ntruencodeschedule, NTRUEncodeSchedule *, ntru_encode_schedule_free(v)) \ + X(shakexof, ShakeXOF *, shake_xof_free(v)) \ /* end of list */ typedef struct Value Value; @@ -743,6 +744,14 @@ strbuf *ssh_hash_final_wrapper(ssh_hash *h) return sb; } +strbuf *shake_xof_read_wrapper(ShakeXOF *sx, TD_uint size) +{ + strbuf *sb = strbuf_new(); + void *p = strbuf_append(sb, size); + shake_xof_read(sx, p, size); + return sb; +} + void ssh_cipher_setiv_wrapper(ssh_cipher *c, ptrlen iv) { if (iv.len != ssh_cipher_alg(c)->blksize) From b36d490b5d0989ad791ace8cf98e36c4a82d5394 Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Sat, 7 Dec 2024 23:19:37 +0000 Subject: [PATCH 15/44] Give the kex selection list box a fixed height. It's actually the limiting factor on how small the whole PuTTY configuration dialog box can be: when KEX_MAX increased from 10 to 11 with the introduction of NTRU, the config box got taller. Now it's back at 10. --- config.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.c b/config.c index 6a20c425..df58f7d6 100644 --- a/config.c +++ b/config.c @@ -2747,7 +2747,7 @@ void setup_config_box(struct controlbox *b, bool midsession, c = ctrl_draglist(s, "Algorithm selection policy:", 's', HELPCTX(ssh_kexlist), kexlist_handler, P(NULL)); - c->listbox.height = KEX_MAX; + c->listbox.height = 10; #ifndef NO_GSSAPI ctrl_checkbox(s, "Attempt GSSAPI key exchange", 'k', HELPCTX(ssh_gssapi), From e98615f0ba2d20607b13169e4dd9966da082139c Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Sat, 7 Dec 2024 19:33:39 +0000 Subject: [PATCH 16/44] New post-quantum kex: ML-KEM, and three hybrids of it. As standardised by NIST in FIPS 203, this is a lattice-based post-quantum KEM. Very vaguely, the idea of it is that your public key is a matrix A and vector t, and the private key is the knowledge of how to decompose t into two vectors with all their coefficients small, one transformed by A relative to the other. Encryption of a binary secret starts by turning each bit into one of two maximally separated residues mod a prime q, and then adding 'noise' based on the public key in the form of small increments and decrements mod q, again with some of the noise transformed by A relative to the rest. Decryption uses the knowledge of t's decomposition to align the two sets of noise so that the _large_ changes (which masked the secret from an eavesdropper) cancel out, leaving only a collection of small changes to the original secret vector. Then the vector of input bits can be recovered by assuming that those accumulated small pieces of noise haven't concentrated in any particular residue enough to push it more than half way to the other of its possible starting values. A weird feature of it is that decryption is not a true mathematical inverse of encryption. The assumption that the noise doesn't get large enough to flip any bit of the secret is only probabilistically valid, not a hard guarantee. In other words, key agreement can fail, simply by getting particularly unlucky with the distribution of your random noise! However, the probability of a failure is very low - less than 2^-138 even for ML-KEM-512, and gets even smaller with the larger variants. An awkward feature for our purposes is that the matrix A, containing a large number of residues mod the prime q=3329, is required to be constructed by a process of rejection sampling, i.e. generating random 12-bit values and throwing away the out-of-range ones. That would be a real pain for our side-channel testing system, which generally handles rejection sampling badly (since it necessarily involves data-dependent control flow and timing variation). Fortunately, the matrix and the random seed it was made from are both public: the matrix seed is transmitted as part of the public key, so it's not necessary to try to hide it. Accordingly, I was able to get the implementation to pass testsc by means of not varying the matrix seed between runs, which is justified by the principle of testsc that you vary the _secrets_ to ensure timing is independent of them - and the matrix seed isn't a secret, so you're allowed to keep it the same. The three hybrid algorithms, defined by the current Internet-Draft draft-kampanakis-curdle-ssh-pq-ke, include one hybrid of ML-KEM-768 with Curve25519 in exactly the same way we were already hybridising NTRU Prime with Curve25519, and two more hybrids of ML-KEM with ECDH over a NIST curve. The former hybrid interoperates with the implementation in OpenSSH 9.9; all three interoperate with the fork 'openssh-oqs' at github.com/open-quantum-safe/openssh, and also with the Python library AsyncSSH. --- config.c | 2 + crypto/CMakeLists.txt | 1 + crypto/kex-hybrid.c | 66 +++ crypto/mlkem.c | 1090 +++++++++++++++++++++++++++++++++++++++++ crypto/mlkem.h | 89 ++++ putty.h | 2 + settings.c | 2 + ssh.h | 12 + ssh/transport2.c | 8 + test/cryptsuite.py | 212 ++++++++ test/testcrypt-enum.h | 6 + test/testcrypt-func.h | 30 ++ test/testcrypt.c | 42 +- test/testcrypt.py | 2 +- test/testsc.c | 58 +++ 15 files changed, 1618 insertions(+), 4 deletions(-) create mode 100644 crypto/mlkem.c create mode 100644 crypto/mlkem.h diff --git a/config.c b/config.c index df58f7d6..f2272068 100644 --- a/config.c +++ b/config.c @@ -578,6 +578,8 @@ static void kexlist_handler(dlgcontrol *ctrl, dlgparam *dlg, { "RSA-based key exchange", KEX_RSA }, { "ECDH key exchange", KEX_ECDH }, { "NTRU Prime / Curve25519 hybrid kex", KEX_NTRU_HYBRID }, + { "ML-KEM / Curve25519 hybrid kex", KEX_MLKEM_25519_HYBRID }, + { "ML-KEM / NIST ECDH hybrid kex", KEX_MLKEM_NIST_HYBRID }, { "-- warn below here --", KEX_WARN } }; diff --git a/crypto/CMakeLists.txt b/crypto/CMakeLists.txt index 0266b2d1..6358569a 100644 --- a/crypto/CMakeLists.txt +++ b/crypto/CMakeLists.txt @@ -24,6 +24,7 @@ add_sources_from_current_dir(crypto mac.c mac_simple.c md5.c + mlkem.c mpint.c ntru.c openssh-certs.c diff --git a/crypto/kex-hybrid.c b/crypto/kex-hybrid.c index 38390bd0..e7291342 100644 --- a/crypto/kex-hybrid.c +++ b/crypto/kex-hybrid.c @@ -36,6 +36,10 @@ static char *hybrid_description(const ssh_kex *kex) 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"); @@ -320,3 +324,65 @@ static const ssh_kex *const ntru_hybrid_list[] = { 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, +}; diff --git a/crypto/mlkem.c b/crypto/mlkem.c new file mode 100644 index 00000000..2074613a --- /dev/null +++ b/crypto/mlkem.c @@ -0,0 +1,1090 @@ +/* + * Implementation of ML-KEM, previously known as 'Crystals: Kyber'. + */ + +#include +#include +#include +#include + +#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] / (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] / (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] / 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] / , 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, "ient, 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), +}; diff --git a/crypto/mlkem.h b/crypto/mlkem.h new file mode 100644 index 00000000..65a677bc --- /dev/null +++ b/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 */ diff --git a/putty.h b/putty.h index df843c47..bd0160fe 100644 --- a/putty.h +++ b/putty.h @@ -388,6 +388,8 @@ enum { KEX_RSA, KEX_ECDH, KEX_NTRU_HYBRID, + KEX_MLKEM_25519_HYBRID, + KEX_MLKEM_NIST_HYBRID, KEX_MAX }; diff --git a/settings.c b/settings.c index ea8853b4..9aee91b5 100644 --- a/settings.c +++ b/settings.c @@ -30,6 +30,8 @@ static const struct keyvalwhere ciphernames[] = { * in sync with those. */ static const struct keyvalwhere kexnames[] = { { "ntru-curve25519", KEX_NTRU_HYBRID, -1, +1 }, + { "mlkem-curve25519", KEX_MLKEM_25519_HYBRID, KEX_NTRU_HYBRID, +1 }, + { "mlkem-nist", KEX_MLKEM_NIST_HYBRID, KEX_MLKEM_25519_HYBRID, +1 }, { "ecdh", KEX_ECDH, -1, +1 }, /* This name is misleading: it covers both SHA-256 and SHA-1 variants */ { "dh-gex-sha1", KEX_DHGEX, -1, -1 }, diff --git a/ssh.h b/ssh.h index 8b1a62f0..dcd111a9 100644 --- a/ssh.h +++ b/ssh.h @@ -1242,6 +1242,11 @@ extern const ssh_kex ssh_ec_kex_nistp521; extern const ssh_kexes ssh_ecdh_kex; extern const ssh_kexes ssh_ntru_hybrid_kex; extern const pq_kemalg ssh_ntru; +extern const ssh_kexes ssh_mlkem_curve25519_hybrid_kex; +extern const ssh_kexes ssh_mlkem_nist_hybrid_kex; +extern const pq_kemalg ssh_mlkem512; +extern const pq_kemalg ssh_mlkem768; +extern const pq_kemalg ssh_mlkem1024; extern const ssh_keyalg ssh_dsa; extern const ssh_keyalg ssh_rsa; extern const ssh_keyalg ssh_rsa_sha256; @@ -1282,6 +1287,13 @@ ssh_hash *blake2b_new_general(unsigned hashlen); /* Special test function for AES-GCM */ void aesgcm_set_prefix_lengths(ssh2_mac *mac, size_t skip, size_t aad); +/* Shake128/256 extendable output functions (like a hash except you don't + * commit up front to how much data you want to get out of it) */ +ShakeXOF *shake128_xof_from_input(ptrlen data); +ShakeXOF *shake256_xof_from_input(ptrlen data); +void shake_xof_read(ShakeXOF *sx, void *output_v, size_t size); +void shake_xof_free(ShakeXOF *sx); + /* * On some systems, you have to detect hardware crypto acceleration by * asking the local OS API rather than OS-agnostically asking the CPU diff --git a/ssh/transport2.c b/ssh/transport2.c index b8e0d1c6..64f40560 100644 --- a/ssh/transport2.c +++ b/ssh/transport2.c @@ -615,6 +615,14 @@ static void ssh2_write_kexinit_lists( preferred_kex[n_preferred_kex++] = &ssh_ntru_hybrid_kex; break; + case KEX_MLKEM_25519_HYBRID: + preferred_kex[n_preferred_kex++] = + &ssh_mlkem_curve25519_hybrid_kex; + break; + case KEX_MLKEM_NIST_HYBRID: + preferred_kex[n_preferred_kex++] = + &ssh_mlkem_nist_hybrid_kex; + break; case KEX_WARN: /* Flag for later. Don't bother if it's the last in * the list. */ diff --git a/test/cryptsuite.py b/test/cryptsuite.py index 5be86193..0add9bec 100755 --- a/test/cryptsuite.py +++ b/test/cryptsuite.py @@ -3447,6 +3447,71 @@ def test(gcm, cbc, iv_fixed, iv_msg): # at the top test(gcm, cbc, 0x27182818, 0xFFFFFFFFFFFFFFFF) + def testMLKEMValidation(self): + # Test validation of hostile inputs (wrong length, + # out-of-range mod q values, mismatching hashes). + for params in 'mlkem512', 'mlkem768', 'mlkem1024': + with self.subTest(params=params): + ek, dk = mlkem_keygen_internal( + params, + b'arbitrary 32-byte test d string!', + b'and another for z, wibbly-wobbly') + + m = b'I suppose we need m as well, ooh' + + # Baseline test: without anything changed, encaps succeeds. + success, c, k = mlkem_encaps_internal(params, ek, m) + self.assertTrue(success) + + # We must check ek has the right length + success, _, _ = mlkem_encaps_internal(params, ek[:-1], m) + self.assertFalse(success) + success, _, _ = mlkem_encaps_internal(params, ek + b'!', m) + self.assertFalse(success) + + # Must reject if a polynomial coefficient is replaced + # with something out of range. Even if it's _only + # just_ out of range, the modulus 3329 itself. So + # replace the first coefficient (first 12 bits) with + # 3329. + ek_bytes = list(ek) + ek_bytes[0] = 3329 & 0xFF + ek_bytes[1] = (ek_bytes[1] & 0xF0) | (3329 >> 8) + success, _, _ = mlkem_encaps_internal( + params, bytes(ek_bytes), m) + self.assertFalse(success) + + # Now do the same with the last polynomial + # coefficient, which occurs 32 bytes before the end of + # ek. (The last 32 bytes are the matrix seed, which + # can be anything.) + ek_bytes = list(ek) + ek_bytes[-33] = 3329 >> 4 + ek_bytes[-34] = (ek_bytes[-34] & 0x0F) | ((3329 << 4) & 0xF0) + success, _, _ = mlkem_encaps_internal( + params, bytes(ek_bytes), m) + self.assertFalse(success) + + # Baseline test of decaps. + self.assertEqual(mlkem_decaps(params, dk, c), (True, k)) + + fail = (False, b'') # expected return value on validation fail + # Modify the length of dk or c, and make sure decaps fails + self.assertEqual(mlkem_decaps(params, dk[:-1], c), fail) + self.assertEqual(mlkem_decaps(params, dk + b'?', c), fail) + self.assertEqual(mlkem_decaps(params, dk, c[:-1]), fail) + self.assertEqual(mlkem_decaps(params, dk, c + b'*'), fail) + + # Tinker with the enclosed copy of ek, and ensure + # that's detected. + eklen = len(ek) + ekstart = len(dk) - 64 - eklen + self.assertEqualBin(dk[ekstart:ekstart+eklen], ek) + dk_bytes = list(dk) + dk_bytes[ekstart] ^= 1 + self.assertEqual( + mlkem_decaps(params, bytes(dk_bytes), c), fail) + class standard_test_vectors(MyTestBase): def testAES(self): def vector(cipher, key, plaintext, ciphertext): @@ -4444,6 +4509,153 @@ def test(key, iv, plaintext, aad, ciphertext, mac): 'c5f61e6393ba7a0abcc9f662'), unhex('76fc6ece0f4e1768cddf8853bb2d551b')) + def testMLKEM(self): + # As of 2024-12-04, a set of ML-KEM test vectors live in a git + # repository at https://github.com/usnistgov/ACVP-Server + # + # Within that repository, the two useful files (as of commit + # 3a7333f638a031c6ed35b6ee31064686eb88c1ec) are: + # gen-val/json-files/ML-KEM-keyGen-FIPS203/internalProjection.json + # gen-val/json-files/ML-KEM-encapDecap-FIPS203/internalProjection.json + # + # The first contains tests of key generation (input randomness + # and the expected output key). The second contains tests of + # encapsulation and decapsulation. + # + # The full set of test cases is too large to transcribe into + # here. But you can run them in full by setting the variable + # names below to local pathnames where those two files can be + # found. + keygen_json_path = None + encapdecap_json_path = None + + def keygen_test(params, d, z, ek_expected, dk_expected): + ek_got, dk_got = mlkem_keygen_internal(params, d, z) + self.assertEqualBin(ek_got, ek_expected) + self.assertEqualBin(dk_got, dk_expected) + + def encaps_test(params, ek, m, c_expected, k_expected): + success, c_got, k_got = mlkem_encaps_internal(params, ek, m) + self.assertTrue(success) + self.assertEqualBin(c_got, c_expected) + self.assertEqualBin(k_got, k_expected) + + def decaps_test(params, dk, c, k_expected): + success, k_got = mlkem_decaps(params, dk, c) + self.assertTrue(success) + self.assertEqualBin(k_got, k_expected) + + if keygen_json_path is not None: + with open(keygen_json_path) as fh: + keygen_json_data = json.load(fh) + for testgroup in keygen_json_data['testGroups']: + # Convert "ML-KEM-768" from the JSON to "mlkem768" + params = testgroup['parameterSet'].lower().replace('-', '') + for testcase in testgroup['tests']: + with self.subTest(testgroup=testgroup['tgId'], + testcase=testcase['tcId']): + keygen_test( + params, + unhex(testcase['d']), unhex(testcase['z']), + unhex(testcase['ek']), unhex(testcase['dk'])) + + if encapdecap_json_path is not None: + with open(encapdecap_json_path) as fh: + encapdecap_json_data = json.load(fh) + for testgroup in encapdecap_json_data['testGroups']: + params = testgroup['parameterSet'].lower().replace('-', '') + for testcase in testgroup['tests']: + with self.subTest(testgroup=testgroup['tgId'], + testcase=testcase['tcId']): + ek = unhex(testcase['ek'] if 'ek' in testcase + else testgroup['ek']) + dk = unhex(testcase['dk'] if 'dk' in testcase + else testgroup['dk']) + c = unhex(testcase['c']) + k = unhex(testcase['k']) + if testgroup["function"] == "encapsulation": + # This is a full test that encapsulates a + # key, decapsulates it at the other end, + # and checks both sides end up with the + # same shared secret. + m = unhex(testcase['m']) + encaps_test(params, ek, m, c, k) + + # All tests include decapsulation. The ones + # that don't also include encapsulation might + # provide _bad_ ciphertexts, to test the + # implicit rejection system. + decaps_test(params, dk, c, k) + + # We replicate a small number of those test cases here, for + # ongoing checks that nothing has broken. + # Keygen test group 1, test case 1 + keygen_test('mlkem512', + d=unhex('2CB843A02EF02EE109305F39119FABF49AB90A57FFECB3A0E75E179450F52761'), + z=unhex('84CC9121AE56FBF39E67ADBD83AD2D3E3BB80843645206BDD9F2F629E3CC49B7'), + ek_expected=unhex('A32439F85A3C21D21A71B9B92A9B64EA0AB84312C77023694FD64EAAB907A43539DDB27BA0A853CC9069EAC8508C653E600B2AC018381B4BB4A879ACDAD342F91179CA8249525CB1968BBE52F755B7F5B43D6663D7A3BF0F3357D8A21D15B52DB3818ECE5B402A60C993E7CF436487B8D2AE91E6C5B88275E75824B0007EF3123C0AB51B5CC61B9B22380DE66C5B20B060CBB986F8123D94060049CDF8036873A7BE109444A0A1CD87A48CAE54192484AF844429C1C58C29AC624CD504F1C44F1E1347822B6F221323859A7F6F754BFE710BDA60276240A4FF2A5350703786F5671F449F20C2A95AE7C2903A42CB3B303FF4C427C08B11B4CD31C418C6D18D0861873BFA0332F11271552ED7C035F0E4BC428C43720B39A65166BA9C2D3D770E130360CC2384E83095B1A159495533F116C7B558B650DB04D5A26EAAA08C3EE57DE45A7F88C6A3CEB24DC5397B88C3CEF003319BB0233FD692FDA1524475B351F3C782182DECF590B7723BE400BE14809C44329963FC46959211D6A623339537848C251669941D90B130258ADF55A720A724E8B6A6CAE3C2264B1624CCBE7B456B30C8C7393294CA5180BC837DD2E45DBD59B6E17B24FE93052EB7C43B27AC3DC249CA0CBCA4FB5897C0B744088A8A0779D32233826A01DD6489952A4825E5358A700BE0E179AC197710D83ECC853E52695E9BF87BB1F6CBD05B02D4E679E3B88DD483B0749B11BD37B383DCCA71F9091834A1695502C4B95FC9118C1CFC34C84C2265BBBC563C282666B60AE5C7F3851D25ECBB5021CC38CB73EB6A3411B1C29046CA66540667D136954460C6FCBC4BC7C049BB047FA67A63B3CC1111C1D8AC27E8058BCCA4A15455858A58358F7A61020BC9C4C17F8B95C268CCB404B9AAB4A272A21A70DAF6B6F15121EE01C156A354AA17087E07702EAB38B3241FDB553F657339D5E29DC5D91B7A5A828EE959FEBB90B07229F6E49D23C3A190297042FB43986955B69C28E1016F77A58B431514D21B888899C3608276081B75F568097CDC1748F32307885815F3AEC9651819AA6873D1A4EB83B1953843B93422519483FEF0059D36BB2DB1F3D468FB068C86E8973733C398EAF00E1702C6734AD8EB3B'), + dk_expected=unhex('7FE4206F26BEDB64C1ED0009615245DC98483F663ACC617E65898D596A8836C49FBD3B4A849759AA1546BDA835CAF175642C28280892A7878CC318BCC75B834CB29FDF5360D7F982A52C88AE914DBF02B58BEB8BA887AE8FAB5EB78731C6757805471EBCEC2E38DB1F4B8310D288920D8A492795A390A74BCD55CD8557B4DAABA82C28CB3F152C5231196193A66A8CCF34B80E1F6942C32BCFF96A6E3CF3939B7B942498CC5E4CB8E8468E702759852AA229C0257F02982097338607C0F0F45446FAB4267993B8A5908CAB9C46780134804AE18815B1020527A222EC4B39A3194E661737791714122662D8B9769F6C67DE625C0D483C3D420FF1BB889A727E756281513A70047648D29C0C30F9BE52EC0DEB977CF0F34FC2078483456964743410638C57B5539577BF85669078C356B3462E9FA5807D49591AFA41C1969F65E3405CB64DDF163F26734CE348B9CF4567A33A5969EB326CFB5ADC695DCA0C8B2A7B1F4F404CC7A0981E2CC24C1C23D16AA9B4392415E26C22F4A934D794C1FB4E5A67051123CCD153764DEC99D553529053C3DA550BCEA3AC54136A26A676D2BA8421067068C6381C2A62A727C933702EE5804A31CA865A45588FB74DE7E2223D88C0608A16BFEC4FAD6752DB56B48B8872BF26BA2FFA0CEDE5343BE8143689265E065F41A6925B86C892E62EB0772734F5A357C75CA1AC6DF78AB1B8885AD0819615376D33EBB98F8733A6755803D977BF51C12740424B2B49C28382A6917CBFA034C3F126A38C216C03C35770AD481B9084B5588DA65FF118A74F932C7E537ABE5863FB29A10C09701B441F8399C1F8A637825ACEA3E93180574FDEB88076661AB46951716A500184A040557266598CAF76105E1C1870B43969C3BCC1A04927638017498BB62CAFD3A6B082B7BF7A23450E191799619B925112D072025CA888548C791AA42251504D5D1C1CDDB213303B049E7346E8D83AD587836F35284E109727E66BBCC9521FE0B191630047D158F75640FFEB5456072740021AFD15A45469C583829DAAC8A7DEB05B24F0567E4317B3E3B33389B5C5F8B04B099FB4D103A32439F85A3C21D21A71B9B92A9B64EA0AB84312C77023694FD64EAAB907A43539DDB27BA0A853CC9069EAC8508C653E600B2AC018381B4BB4A879ACDAD342F91179CA8249525CB1968BBE52F755B7F5B43D6663D7A3BF0F3357D8A21D15B52DB3818ECE5B402A60C993E7CF436487B8D2AE91E6C5B88275E75824B0007EF3123C0AB51B5CC61B9B22380DE66C5B20B060CBB986F8123D94060049CDF8036873A7BE109444A0A1CD87A48CAE54192484AF844429C1C58C29AC624CD504F1C44F1E1347822B6F221323859A7F6F754BFE710BDA60276240A4FF2A5350703786F5671F449F20C2A95AE7C2903A42CB3B303FF4C427C08B11B4CD31C418C6D18D0861873BFA0332F11271552ED7C035F0E4BC428C43720B39A65166BA9C2D3D770E130360CC2384E83095B1A159495533F116C7B558B650DB04D5A26EAAA08C3EE57DE45A7F88C6A3CEB24DC5397B88C3CEF003319BB0233FD692FDA1524475B351F3C782182DECF590B7723BE400BE14809C44329963FC46959211D6A623339537848C251669941D90B130258ADF55A720A724E8B6A6CAE3C2264B1624CCBE7B456B30C8C7393294CA5180BC837DD2E45DBD59B6E17B24FE93052EB7C43B27AC3DC249CA0CBCA4FB5897C0B744088A8A0779D32233826A01DD6489952A4825E5358A700BE0E179AC197710D83ECC853E52695E9BF87BB1F6CBD05B02D4E679E3B88DD483B0749B11BD37B383DCCA71F9091834A1695502C4B95FC9118C1CFC34C84C2265BBBC563C282666B60AE5C7F3851D25ECBB5021CC38CB73EB6A3411B1C29046CA66540667D136954460C6FCBC4BC7C049BB047FA67A63B3CC1111C1D8AC27E8058BCCA4A15455858A58358F7A61020BC9C4C17F8B95C268CCB404B9AAB4A272A21A70DAF6B6F15121EE01C156A354AA17087E07702EAB38B3241FDB553F657339D5E29DC5D91B7A5A828EE959FEBB90B07229F6E49D23C3A190297042FB43986955B69C28E1016F77A58B431514D21B888899C3608276081B75F568097CDC1748F32307885815F3AEC9651819AA6873D1A4EB83B1953843B93422519483FEF0059D36BB2DB1F3D468FB068C86E8973733C398EAF00E1702C6734AD8EB3B620130D6C2B8C904A3BB9307BE5103F8D814505FB6A60AF7937EA6CAA117315E84CC9121AE56FBF39E67ADBD83AD2D3E3BB80843645206BDD9F2F629E3CC49B7')) + # Keygen test group 2, test case 26 + keygen_test('mlkem768', + d=unhex('E34A701C4C87582F42264EE422D3C684D97611F2523EFE0C998AF05056D693DC'), + z=unhex('A85768F3486BD32A01BF9A8F21EA938E648EAE4E5448C34C3EB88820B159EEDD'), + ek_expected=unhex('6D14A071F7CC452558D5E71A7B087062ECB1386844588246126402B1FA1637733CD5F60CC84BCB646A7892614D7C51B1C7F1A2799132F13427DC482158DA254470A59E00A4E49686FDC077559367270C2153F11007592C9C4310CF8A12C6A8713BD6BB51F3124F989BA0D54073CC242E0968780B875A869EFB851586B9A868A384B9E6821B201B932C455369A739EC22569C977C212B381871813656AF5B567EF893B584624C863A259000F17B254B98B185097C50EBB68B244342E05D4DE520125B8E1033B1436093ACE7CE8E71B458D525673363045A3B3EEA9455428A398705A42327ADB3774B7057F42B017EC0739A983F19E8214D09195FA24D2D571DB73C19A6F8460E50830D415F627B88E94A7B153791A0C0C7E9484C74D53C714889F0E321B6660A532A5BC0E557FBCA35E29BC611200ED3C633077A4D873C5CC67006B753BF6D6B7AF6CA402AB618236C0AFFBC801F8222FBC36CE0984E2B18C944BBCBEF03B1E1361C1F44B0D734AFB1566CFF8744DA8B9943D6B45A3C09030702CA201FFE20CB7EC5B0D4149EE2C28E8B23374F471B57150D0EC9336261A2D5CB84A3ACACC4289473A4C0ABC617C9ABC178734434C82E1685588A5C2EA2678F6B3C2228733130C466E5B86EF491153E48662247B875D201020B566B81B64D839AB4633BAA8ACE202BAAB4496297F9807ADBBB1E332C6F8022B2A18CFDD4A82530B6D3F007C3353898D966CC2C21CB4244BD00443F209870ACC42BC33068C724EC17223619C1093CCA6AEB29500664D1225036B4B81091906969481F1C723C140B9D6C168F5B64BEA69C5FD6385DF7364B8723BCC85E038C7E464A900D68A2127818994217AEC8BDB39A970A9963DE93688E2AC82ABCC22FB9277BA22009E878381A38163901C7D4C85019538D35CAAE9C41AF8C929EE20BB08CA619E72C2F2262C1C9938572551AC02DC9268FBCC35D79011C3C090AD40A4F111C9BE55C427EB796C1932D8673579AF1B4C638B0944489012A2559A3B02481B01AC30BA8960F80C0C2B3947D36A12C080498BEE448716C973416C8242804A3DA099EE137B0BA90FE4A5C6A89200276A0CFB643EC2C56A2D708D7B4373E44C1502A763A600586E6CDA6273897D44448287DC2E602DC39200BF6166236559FD12A60892AEB153DD651BB469910B4B34669F91DA8654D1EB72EB6E02800B3B0A7D0A48C836854D3A83E65569CB7230BB44F3F143A6DEC5F2C39AB90F274F2088BD3D6A6FCA0070273BEDC84777FB52E3C558B0AE06183D5A48D452F68E15207F861627ACA14279630F82EC3A0CA078633B600AFA79743A600215BE5637458CE2CE8AFF5A08EB5017B2C766577479F8DC6BF9F5CC75089932161B96CEA406620AEDB630407F7687EBBB4814C7981637A48A90DE68031E062A7AF7612B4F5C7A6DA86BD136529E64295A5613EA73BD3D4448CB81F243135C0A660BEB9C17E651DEF469A7D90A15D3481090BCBF227012328941FA46F39C5006AD93D458AA6ADD655862B418C3094F551460DF2153A5810A7DA74F0614C2588BE49DC6F5E88154642BD1D3762563326433507156A57C57694BDD26E7A246FEB723AED67B04887C8E476B48CAB59E5362F26A9EF50C2BC80BA146226216FE62968A60D04E8C170D741C7A2B0E1ABDAC968'), + dk_expected=unhex('98A1B2DA4A65CFB5845EA7311E6A06DB731F1590C41EE74BA10782715B35A3102DF637872BE65BAB37A1DE2511D703C70247B35EF27435485024D93FD9E77C43804F371749BA00B20A8C5C588BC9ABE068AEAAA938517EBFE53B6B663282903DCD189736D7296816C733A1C77C6375E5397C0F189BBFE47643A61F58F8A3C6911BE4611A8C7BC050021163D0A404DC14065748FF29BE60D2B9FDCC8FFD98C587F38C67115786464BDB342B17E897D64617CBFB117973A5458977A7D7617A1B4D83BA03C611138A4673B1EB34B078033F97CFFE80C146A26943F842B976327BF1CBC60119525BB9A3C03493349000DD8F51BA21A2E92361762324600E0C13AAA6CB69BFB24276483F6B02421259B7585263C1A028D682C508BBC2801A56E98B8F620B0483D79B5AD8585AC0A475BAC77865194196338791B7985A05D109395CCA8932722A91950D37E12B891420A52B62CBFA815DF6174CE00E68BCA75D4838CA280F713C7E6924AFD95BAA0D01ADA637B158347034C0AB1A7183331A820ACBCB83193A1A94C8F7E384AED0C35ED3CB3397BB638086E7A35A6408A3A4B90CE953707C19BC46C3B2DA3B2EE32319C56B928032B5ED1256D0753D341423E9DB139DE7714FF075CAF58FD9F57D1A54019B5926406830DAE29A875302A81256F4D6CF5E74034EA614BF70C2764B20C9589CDB5C25761A04E58292907C578A94A35836BEE3112DC2C3AE2192C9DEAA304B29C7FEA1BDF47B3B6BCBA2C0E55C9CDB6DE7149E9CB17917718F12C8032DE1ADE0648D405519C70719BECC701845CF9F4B912FE71983CA34F9018C7CA7BB2F6C5D7F8C5B297359EC75209C2543FF11C4244977C5969524EC454D44C323FCCA94ACAC273A0EC49B4A8A585BCE7A5B305C04C3506422580357016A850C3F7EE17205A77B291C7731C9836C02AEE5406F63C6A07A214382AA15336C05D1045588107645EA7DE6870FC0E55E1540974301C42EC14105518680F688ABE4CE453738FE471B87FC31F5C68A39E68AF51B0240B90E0364B04BAC43D6FB68AB65AE028B62BD683B7D28AD38806BEE725B5B2416A8D79C16EC2A99EA4A8D92A2F5052E67F97352289761C5C39FC5C742E9C0A740CA59FC0182F709D01B5187F00063DAAB397596EEA4A31BDBCBD4C1BB0C55BE7C6850FDA9326B353E288C5013226C3C3923A791609E8002E73A5F7B6BB4A877B1FDF53BB2BAB3DD424D31BBB448E609A66B0E343C286E8760312B6D37AA5201D21F53503D88389ADCA21C70FB6C0FC9C69D6616C9EA3780E35565C0C97C15179C95343ECC5E1C2A24DE4699F6875EA2FA2DD3E357BC43914795207E026B850A2237950C108A512FC88C22488112607088185FB0E09C2C4197A83687266BAB2E583E21C40F4CC008FE652804D8223F1520A90B0D5385C7553CC767C58D120CCD3EF5B5D1A6CD7BC00DFF1321B2F2C432B64EFB8A3F5D0064B3F34293026C851C2DED68B9DFF4A28F6A8D225535E0477084430CFFDA0AC0552F9A212785B749913A06FA2274C0D15BAD325458D323EF6BAE13C0010D525C1D5269973AC29BDA7C983746918BA0E002588E30375D78329E6B8BA8C4462A692FB6083842B8C8C92C60F252726D14A071F7CC452558D5E71A7B087062ECB1386844588246126402B1FA1637733CD5F60CC84BCB646A7892614D7C51B1C7F1A2799132F13427DC482158DA254470A59E00A4E49686FDC077559367270C2153F11007592C9C4310CF8A12C6A8713BD6BB51F3124F989BA0D54073CC242E0968780B875A869EFB851586B9A868A384B9E6821B201B932C455369A739EC22569C977C212B381871813656AF5B567EF893B584624C863A259000F17B254B98B185097C50EBB68B244342E05D4DE520125B8E1033B1436093ACE7CE8E71B458D525673363045A3B3EEA9455428A398705A42327ADB3774B7057F42B017EC0739A983F19E8214D09195FA24D2D571DB73C19A6F8460E50830D415F627B88E94A7B153791A0C0C7E9484C74D53C714889F0E321B6660A532A5BC0E557FBCA35E29BC611200ED3C633077A4D873C5CC67006B753BF6D6B7AF6CA402AB618236C0AFFBC801F8222FBC36CE0984E2B18C944BBCBEF03B1E1361C1F44B0D734AFB1566CFF8744DA8B9943D6B45A3C09030702CA201FFE20CB7EC5B0D4149EE2C28E8B23374F471B57150D0EC9336261A2D5CB84A3ACACC4289473A4C0ABC617C9ABC178734434C82E1685588A5C2EA2678F6B3C2228733130C466E5B86EF491153E48662247B875D201020B566B81B64D839AB4633BAA8ACE202BAAB4496297F9807ADBBB1E332C6F8022B2A18CFDD4A82530B6D3F007C3353898D966CC2C21CB4244BD00443F209870ACC42BC33068C724EC17223619C1093CCA6AEB29500664D1225036B4B81091906969481F1C723C140B9D6C168F5B64BEA69C5FD6385DF7364B8723BCC85E038C7E464A900D68A2127818994217AEC8BDB39A970A9963DE93688E2AC82ABCC22FB9277BA22009E878381A38163901C7D4C85019538D35CAAE9C41AF8C929EE20BB08CA619E72C2F2262C1C9938572551AC02DC9268FBCC35D79011C3C090AD40A4F111C9BE55C427EB796C1932D8673579AF1B4C638B0944489012A2559A3B02481B01AC30BA8960F80C0C2B3947D36A12C080498BEE448716C973416C8242804A3DA099EE137B0BA90FE4A5C6A89200276A0CFB643EC2C56A2D708D7B4373E44C1502A763A600586E6CDA6273897D44448287DC2E602DC39200BF6166236559FD12A60892AEB153DD651BB469910B4B34669F91DA8654D1EB72EB6E02800B3B0A7D0A48C836854D3A83E65569CB7230BB44F3F143A6DEC5F2C39AB90F274F2088BD3D6A6FCA0070273BEDC84777FB52E3C558B0AE06183D5A48D452F68E15207F861627ACA14279630F82EC3A0CA078633B600AFA79743A600215BE5637458CE2CE8AFF5A08EB5017B2C766577479F8DC6BF9F5CC75089932161B96CEA406620AEDB630407F7687EBBB4814C7981637A48A90DE68031E062A7AF7612B4F5C7A6DA86BD136529E64295A5613EA73BD3D4448CB81F243135C0A660BEB9C17E651DEF469A7D90A15D3481090BCBF227012328941FA46F39C5006AD93D458AA6ADD655862B418C3094F551460DF2153A5810A7DA74F0614C2588BE49DC6F5E88154642BD1D3762563326433507156A57C57694BDD26E7A246FEB723AED67B04887C8E476B48CAB59E5362F26A9EF50C2BC80BA146226216FE62968A60D04E8C170D741C7A2B0E1ABDAC968E29020839D052FA372585627F8B59EE312AE414C979D825F06A6929A79625718A85768F3486BD32A01BF9A8F21EA938E648EAE4E5448C34C3EB88820B159EEDD')) + # Keygen test group 3, test case 51 + keygen_test('mlkem1024', + d=unhex('49AC8B99BB1E6A8EA818261F8BE68BDEAA52897E7EC6C40B530BC760AB77DCE3'), + z=unhex('99E3246884181F8E1DD44E0C7629093330221FD67D9B7D6E1510B2DBAD8762F7'), + ek_expected=unhex('A04184D4BC7B532A0F70A54D7757CDE6175A6843B861CB2BC4830C0012554CFC5D2C8A2027AA3CD967130E9B96241B11C4320C7649CC23A71BAFE691AFC08E680BCEF42907000718E4EACE8DA28214197BE1C269DA9CB541E1A3CE97CFADF9C6058780FE6793DBFA8218A2760B802B8DA2AA271A38772523A76736A7A31B9D3037AD21CEBB11A472B8792EB17558B940E70883F264592C689B240BB43D5408BF446432F412F4B9A5F6865CC252A43CF40A320391555591D67561FDD05353AB6B019B3A08A73353D51B6113AB2FA51D975648EE254AF89A230504A236A4658257740BDCBBE1708AB022C3C588A410DB3B9C308A06275BDF5B4859D3A2617A295E1A22F90198BAD0166F4A943417C5B831736CB2C8580ABFDE5714B586ABEEC0A175A08BC710C7A2895DE93AC438061BF7765D0D21CD418167CAF89D1EFC3448BCBB96D69B3E010C82D15CAB6CACC6799D3639669A5B21A633C865F8593B5B7BC800262BB837A924A6C5440E4FC73B41B23092C3912F4C6BEBB4C7B4C62908B03775666C22220DF9C88823E344C7308332345C8B795D34E8C051F21F5A21C214B69841358709B1C305B32CC2C3806AE9CCD3819FFF4507FE520FBFC27199BC23BE6B9B2D2AC1717579AC769279E2A7AAC68A371A47BA3A7DBE016F14E1A727333663C4A5CD1A0F8836CF7B5C49AC51485CA60345C990E06888720003731322C5B8CD5E6907FDA1157F468FD3FC20FA8175EEC95C291A262BA8C5BE990872418930852339D88A19B37FEFA3CFE82175C224407CA414BAEB37923B4D2D83134AE154E490A9B45A0563B06C953C3301450A2176A07C614A74E3478E48509F9A60AE945A8EBC7815121D90A3B0E07091A096CF02C57B25BCA58126AD0C629CE166A7EDB4B33221A0D3F72B85D562EC698B7D0A913D73806F1C5C87B38EC003CB303A3DC51B4B35356A67826D6EDAA8FEB93B98493B2D1C11B676A6AD9506A1AAAE13A824C7C08D1C6C2C4DBA9642C76EA7F6C8264B64A23CCCA9A74635FCBF03E00F1B5722B214376790793B2C4F0A13B5C40760B4218E1D2594DCB30A70D9C1782A5DD30576FA4144BFC8416EDA8118FC6472F56A979586F33BB070FB0F1B0B10BC4897EBE01BCA3893D4E16ADB25093A7417D0708C83A26322E22E6330091E30152BF823597C04CCF4CFC7331578F43A2726CCB428289A90C863259DD180C5FF142BEF41C7717094BE07856DA2B140FA67710967356AA47DFBC8D255B4722AB86D439B7E0A6090251D2D4C1ED5F20BBE6807BF65A90B7CB2EC0102AF02809DC9AC7D0A3ABC69C18365BCFF59185F33996887746185906C0191AED4407E139446459BE29C6822717644353D24AB6339156A9C424909F0A9025BB74720779BE43F16D81C8CC666E99710D8C68BB5CC4E12F314E925A551F09CC59003A1F88103C254BB978D75F394D3540E31E771CDA36E39EC54A62B5832664D821A72F1E6AFBBA27F84295B2694C498498E812BC8E9378FE541CEC5891B25062901CB7212E3CDC46179EC5BCEC10BC0B9311DE05074290687FD6A5392671654284CD9C8CC3EBA80EB3B662EB53EB75116704A1FEB5C2D056338532868DDF24EB8992AB8565D9E490CADF14804360DAA90718EAB616BAB0765D33987B47EFB6599C5563235E61E4BE670E97955AB292D9732CB8930948AC82DF230AC72297A23679D6B94C17F1359483254FEDC2F05819F0D069A443B78E3FC6C3EF4714B05A3FCA81CBBA60242A7060CD885D8F39981BB18092B23DAA59FD9578388688A09BBA079BC809A54843A60385E2310BBCBCC0213CE3DFAAB33B47F9D6305BC95C6107813C585C4B657BF30542833B14949F573C0612AD524BAAE69590C1277B86C286571BF66B3CFF46A3858C09906A794DF4A06E9D4B0A2E43F10F72A6C6C47E5646E2C799B71C33ED2F01EEB45938EB7A4E2E2908C53558A540D350369FA189C616943F7981D7618CF02A5B0A2BCC422E857D1A47871253D08293C1C179BCDC0437069107418205FDB9856623B8CA6B694C96C084B17F13BB6DF12B2CFBBC2B0E0C34B00D0FCD0AECFB27924F6984E747BE2A09D83A8664590A8077331491A4F7D720843F23E652C6FA840308DB4020337AAD37967034A9FB523B67CA70330F02D9EA20C1E84CB8E5757C9E1896B60581441ED618AA5B26DA56C0A5A73C4DCFD755E610B4FC81FF84E21'), + dk_expected=unhex('8C8B3722A82E550565521611EBBC63079944C9B1ABB3B0020FF12F631891A9C468D3A67BF6271280DA58D03CB042B3A461441637F929C273469AD15311E910DE18CB9537BA1BE42E98BB59E498A13FD440D0E69EE832B45CD95C382177D67096A18C07F1781663651BDCAC90DEDA3DDD143485864181C91FA2080F6DAB3F86204CEB64A7B4446895C03987A031CB4B6D9E0462FDA829172B6C012C638B29B5CD75A2C930A5596A3181C33A22D574D30261196BC350738D4FD9183A763336243ACED99B3221C71D8866895C4E52C119BF3280DAF80A95E15209A795C4435FBB3570FDB8AA9BF9AEFD43B094B781D5A81136DAB88B8799696556FEC6AE14B0BB8BE4695E9A124C2AB8FF4AB1229B8AAA8C6F41A60C34C7B56182C55C2C685E737C6CA00A23FB8A68C1CD61F30D3993A1653C1675AC5F0901A7160A73966408B8876B715396CFA4903FC69D60491F8146808C97CD5C533E71017909E97B835B86FF847B42A696375435E006061CF7A479463272114A89EB3EAF2246F0F8C104A14986828E0AD20420C9B37EA23F5C514949E77AD9E9AD12290DD1215E11DA274457AC86B1CE6864B122677F3718AA31B02580E64317178D38F25F609BC6C55BC374A1BF78EA8ECC219B30B74CBB3272A599238C93985170048F176775FB19962AC3B135AA59DB104F7114DBC2C2D42949ADECA6A85B323EE2B2B23A77D9DB235979A8E2D67CF7D2136BBBA71F269574B38888E1541340C19284074F9B7C8CF37EB01384E6E3822EC4882DFBBEC4E6098EF2B2FC177A1F0BCB65A57FDAA89315461BEB7885FB68B3CD096EDA596AC0E61DD7A9C507BC6345E0827DFCC8A3AC2DCE51AD731AA0EB932A6D0983992347CBEB3CD0D9C9719797CC21CF0062B0AD94CAD734C63E6B5D859CBE19F0368245351BF464D7505569790D2BB724D8659A9FEB1C7C473DC4D061E29863A2714BAC42ADCD1A8372776556F7928A7A44E94B6A25322D03C0A1622A7FD261522B7358F085BDFB60758762CB901031901B5EECF4920C81020A9B1781BCB9DD19A9DFB66458E7757C52CEC75B4BA740A24099CB56BB60A76B6901AA3E0169C9E83496D73C4C99435A28D613E97A1177F58B6CC595D3B2331E9CA7B57B74DC2C5277D26F2FE19240A55C35D6CFCA26C73E9A2D7C980D97960AE1A04698C16B398A5F20C35A0914145CE1674B71ABC6066A909A3E4B911E69D5A849430361F731B07246A6329B52361904225082D0AAC5B21D6B34862481A890C3C360766F04263603A6B73E802B1F70B2EB00046836B8F493BF10B90B8737C6C548449B294C47253BE26CA72336A632063AD3D0B48C8B0F4A34447EF13B764020DE739EB79ABA20E2BE1951825F293BEDD1089FCB0A91F560C8E17CDF52541DC2B81F972A7375B201F10C08D9B5BC8B95100054A3D0AAFF89BD08D6A0E7F2115A435231290460C9AD435A3B3CF35E52091EDD1890047BCC0AABB1ACEBC75F4A32BC1451ACC4969940788E89412188946C9143C5046BD1B458DF617C5DF533B052CD6038B7754034A23C2F7720134C7B4EACE01FAC0A2853A9285847ABBD06A3343A778AC6062E458BC5E61ECE1C0DE0206E6FE8A84034A7C5F1B005FB0A584051D3229B86C909AC5647B3D75569E05A88279D80E5C30F574DC327512C6BBE8101239EC62861F4BE67B05B9CDA9C545C13E7EB53CFF260AD9870199C21F8C63D64F0458A7141285023FEB829290872389644B0C3B73AC2C8E121A29BB1C43C19A233D56BED82740EB021C97B8EBBA40FF328B541760FCC372B52D3BC4FCBC06F424EAF253804D4CB46F41FF254C0C5BA483B44A87C219654555EC7C163C79B9CB760A2AD9BB722B93E0C28BD4B1685949C496EAB1AFF90919E3761B346838ABB2F01A91E554375AFDAAAF3826E6DB79FE7353A7A578A7C0598CE28B6D9915214236BBFFA6D45B6376A07924A39A7BE818286715C8A3C110CD76C02E0417AF138BDB95C3CCA798AC809ED69CFB672B6FDDC24D89C06A6558814AB0C21C62B2F84C0E3E0803DB337A4E0C7127A6B4C8C08B1D1A76BF07EB6E5B5BB47A16C74BC548375FB29CD789A5CFF91BDBD071859F4846E355BB0D29484E264DFF36C9177A7ACA78908879695CA87F25436BC12630724BB22F0CB64897FE5C41195280DA04184D4BC7B532A0F70A54D7757CDE6175A6843B861CB2BC4830C0012554CFC5D2C8A2027AA3CD967130E9B96241B11C4320C7649CC23A71BAFE691AFC08E680BCEF42907000718E4EACE8DA28214197BE1C269DA9CB541E1A3CE97CFADF9C6058780FE6793DBFA8218A2760B802B8DA2AA271A38772523A76736A7A31B9D3037AD21CEBB11A472B8792EB17558B940E70883F264592C689B240BB43D5408BF446432F412F4B9A5F6865CC252A43CF40A320391555591D67561FDD05353AB6B019B3A08A73353D51B6113AB2FA51D975648EE254AF89A230504A236A4658257740BDCBBE1708AB022C3C588A410DB3B9C308A06275BDF5B4859D3A2617A295E1A22F90198BAD0166F4A943417C5B831736CB2C8580ABFDE5714B586ABEEC0A175A08BC710C7A2895DE93AC438061BF7765D0D21CD418167CAF89D1EFC3448BCBB96D69B3E010C82D15CAB6CACC6799D3639669A5B21A633C865F8593B5B7BC800262BB837A924A6C5440E4FC73B41B23092C3912F4C6BEBB4C7B4C62908B03775666C22220DF9C88823E344C7308332345C8B795D34E8C051F21F5A21C214B69841358709B1C305B32CC2C3806AE9CCD3819FFF4507FE520FBFC27199BC23BE6B9B2D2AC1717579AC769279E2A7AAC68A371A47BA3A7DBE016F14E1A727333663C4A5CD1A0F8836CF7B5C49AC51485CA60345C990E06888720003731322C5B8CD5E6907FDA1157F468FD3FC20FA8175EEC95C291A262BA8C5BE990872418930852339D88A19B37FEFA3CFE82175C224407CA414BAEB37923B4D2D83134AE154E490A9B45A0563B06C953C3301450A2176A07C614A74E3478E48509F9A60AE945A8EBC7815121D90A3B0E07091A096CF02C57B25BCA58126AD0C629CE166A7EDB4B33221A0D3F72B85D562EC698B7D0A913D73806F1C5C87B38EC003CB303A3DC51B4B35356A67826D6EDAA8FEB93B98493B2D1C11B676A6AD9506A1AAAE13A824C7C08D1C6C2C4DBA9642C76EA7F6C8264B64A23CCCA9A74635FCBF03E00F1B5722B214376790793B2C4F0A13B5C40760B4218E1D2594DCB30A70D9C1782A5DD30576FA4144BFC8416EDA8118FC6472F56A979586F33BB070FB0F1B0B10BC4897EBE01BCA3893D4E16ADB25093A7417D0708C83A26322E22E6330091E30152BF823597C04CCF4CFC7331578F43A2726CCB428289A90C863259DD180C5FF142BEF41C7717094BE07856DA2B140FA67710967356AA47DFBC8D255B4722AB86D439B7E0A6090251D2D4C1ED5F20BBE6807BF65A90B7CB2EC0102AF02809DC9AC7D0A3ABC69C18365BCFF59185F33996887746185906C0191AED4407E139446459BE29C6822717644353D24AB6339156A9C424909F0A9025BB74720779BE43F16D81C8CC666E99710D8C68BB5CC4E12F314E925A551F09CC59003A1F88103C254BB978D75F394D3540E31E771CDA36E39EC54A62B5832664D821A72F1E6AFBBA27F84295B2694C498498E812BC8E9378FE541CEC5891B25062901CB7212E3CDC46179EC5BCEC10BC0B9311DE05074290687FD6A5392671654284CD9C8CC3EBA80EB3B662EB53EB75116704A1FEB5C2D056338532868DDF24EB8992AB8565D9E490CADF14804360DAA90718EAB616BAB0765D33987B47EFB6599C5563235E61E4BE670E97955AB292D9732CB8930948AC82DF230AC72297A23679D6B94C17F1359483254FEDC2F05819F0D069A443B78E3FC6C3EF4714B05A3FCA81CBBA60242A7060CD885D8F39981BB18092B23DAA59FD9578388688A09BBA079BC809A54843A60385E2310BBCBCC0213CE3DFAAB33B47F9D6305BC95C6107813C585C4B657BF30542833B14949F573C0612AD524BAAE69590C1277B86C286571BF66B3CFF46A3858C09906A794DF4A06E9D4B0A2E43F10F72A6C6C47E5646E2C799B71C33ED2F01EEB45938EB7A4E2E2908C53558A540D350369FA189C616943F7981D7618CF02A5B0A2BCC422E857D1A47871253D08293C1C179BCDC0437069107418205FDB9856623B8CA6B694C96C084B17F13BB6DF12B2CFBBC2B0E0C34B00D0FCD0AECFB27924F6984E747BE2A09D83A8664590A8077331491A4F7D720843F23E652C6FA840308DB4020337AAD37967034A9FB523B67CA70330F02D9EA20C1E84CB8E5757C9E1896B60581441ED618AA5B26DA56C0A5A73C4DCFD755E610B4FC81FF84E21D2E574DFD8CD0AE893AA7E125B44B924F45223EC09F2AD1141EA93A68050DBF699E3246884181F8E1DD44E0C7629093330221FD67D9B7D6E1510B2DBAD8762F7')) + # Encaps test from test group 1, test case 1 + encaps_test('mlkem512', + ek=unhex('dd1924935aa8e617af18b5a065ac45727767ee897cf4f9442b2ace30c0237b307d3e76bf8eeb78addc4aacd16463d8602fd5487b63c88bb66027f37d0d614d6f9c24603c42947664ac4398c6c52383469b4f9777e5ec7206210f3e5a796bf45c53268e25f39ac261af3bfa2ee755beb8b67ab3ac8df6c629c1176e9e3b965e9369f9b3b92ad7c20955641d99526fe7b9fe8c850820275cd964849250090733ce124ecf316624374bd18b7c358c06e9c136ee1259a9245abc55b964d689f5a08292d28265658ebb40cbfe488a2228275590ab9f32a34109709c1c291d4a23337274c7a5a5991c7a87b81c974ab18ce77859e4995e7c14f0371748b7712fb52c5966cd63063c4f3b81b47c45dde83fb3a2724029b10b3230214c04fa0577fc29ac9086ae18c53b3ed44e507412fca04b4f538a51588ec1f1029d152d9ae7735f76a077aa9484380aed9189e5912487fcc5b7c7012d9223dd967eecdac3008a8931b648243537f548c171698c5b381d846a72e5c92d4226c5a8909884f1c4a3404c1720a5279414d7f27b2b982652b6740219c56d217780d7a5e5ba59836349f726881dea18ef75c0772a8b922766953718cacc14ccbacb5fc412a2d0be521817645ab2bf6a4785e92bc94caf477a967876796c0a5190315ac0885671a4c749564c3b2c7aed9064eba299ef214ba2f40493667c8bd032aec5621711b41a3852c5c2bab4a349ce4b7f085a812bbbc820b81befe63a05b8bcdfe9c2a70a8b1aca9bf9816481907ff4432461111287303f0bd817c05726bfa18a2e24c7724921028032f622bd960a317d83b356b57f4a8004499cbc73c97d1eb7745972631c0561c1a3ab6ef91bd363280a10545da693e6d58aed6845e7cc5f0d08ca7905052c77366d1972ccfcc1a27610cb543665aa798e20940128b9567a7edb7a900407c70d359438435e13961608d552a94c5cda7859220509b483c5c52a210e9c812bc0c2328ca00e789a56b2606b90292e3543dacaa2431841d61a22ca90c1ccf0b5b4e0a6f640536d1a26ab5b8d2151327928ce02904cf1d15e32788a95f62d3c270b6fa1508f97b9155a2726d80a1afa3c5387a276a4d031a08abf4f2e74f1a0bb8a0fd3cb'), + m=unhex('6ff02e1dc7fd911beee0c692c8bd100c3e5c48964d31df92994218e80664a6ca'), + c_expected=unhex('19c592505907c24c5fa2ebfa932d2cbb48f3e4340a28f7eba5d068fcacabedf77784e2b24d7961775f0bf1a997ae8ba9fc4311be63716779c2b788f812cbb78c74e7517e22e910eff5f38d44469c50de1675ae198fd6a289ae7e6c30a9d4351b3d1f4c36eff9c68da91c40b82dc9b2799a33a26b60a4e70d7101862779469f3a9daec8e3e8f8c6a16bf092fba5866186b8d208fdeb274ac1f829659dc2be4ac4f306cb5584bad1936a92c9b76819234281bb395841c25756086ea564ca3e227e3d9f1052c0766d2eb79a47c150721e0dea7c0069d551b264801b7727ecaf82eecb99a876fda090bf6c3fc6b109f1701485f03ce66274b8435b0a014cfb3e79cced67057b5ae2ad7f5279eb714942e4c1ccff7e85c0db43e5d41289207363b444bb51bb8ab0371e70cbd55f0f3dad403e105176e3e8a225d84ac8bee38c821ee0f547431145dcb3139286abb11794a43a3c1b5229e4bcfe959c78adaee2d5f2497b5d24bc21fa03a9a58c2455373ec89583e7e588d7fe67991ee93783ed4a6f9eeae04e64e2e1e0e699f6dc9c5d39ef9278c985e7fdf2a764ffd1a0b95792ad681e930d76df4efe5d65dbbd0f1438481ed833ad4946ad1c69ad21dd7c86185774426f3fcf53b52ad4b40d228ce124072f592c7daa057f17d790a5bd5b93834d58c08c88dc8f0ef488156425b744654eaca9d64858a4d6ceb478795194bfadb18dc0ea054f9771215ad3cb1fd031d7be4598621926478d375a1845aa91d7c733f8f0e188c83896edf83b8646c99e29c0da2290e71c3d2e970720c97b5b7f950486033c6a2571ddf2bccdabb2dfa5fce4c3a1884606041d181c728794ae0e806ecb49af16756a4ce73c87bd4234e60f05535fa5929fd5a34473266401f63bbd6b90e003472ac0ce88f1b666597279d056a632c8d6b790fd411767848a69e37a8a839bc766a02ca2f695ec63f056a4e2a114cacf9fd90d730c970db387f6de73395f701a1d953b2a89dd7edad439fc205a54a481e889b098d5255670f026b4a2bf02d2bdde87c766b25fc5e0fd453757e756d18c8cd912f9a77f8e6bf0205374b462'), + k_expected=unhex('0bf323338d6f0a21d5514b673cd10b714ce6e36f35bcd1bf544196368ee51a13')) + # Encaps test from test group 2, test case 26 + encaps_test('mlkem768', + ek=unhex('89d2cb65f94dcbfc890efc7d0e5a7a38344d1641a3d0b024d50797a5f23c3a18b3101a1269069f43a842bacc098a8821271c673db1beb33034e4d7774d16635c7c2c3c2763453538bc1632e1851591a51642974e5928abb8e55fe55612f9b141aff015545394b2092e590970ec29a7b7e7aa1fb4493bf7cb731906c2a5cb49e6614859064e19b8fa26af51c44b5e7535bfdac072b646d3ea490d277f0d97ced47395fed91e8f2bce0e3ca122c2025f74067ab928a822b35653a74f06757629afb1a1caf237100ea935e793c8f58a71b3d6ae2c8658b10150d4a38f572a0d49d28ae89451d338326fdb3b4350036c1081117740edb86b12081c5c1223dbb5660d5b3cb3787d481849304c68be875466f14ee5495c2bd795ae412d09002d65b8719b90cba3603ac4958ea03cc138c86f7851593125334701b677f82f4952a4c93b5b4c134bb42a857fd15c650864a6aa94eb691c0b691be4684c1f5b7490467fc01b1d1fda4dda35c4ecc231bc73a6fef42c99d34eb82a4d014987b3e386910c62679a118f3c5bd9f467e4162042424357db92ef484a4a1798c1257e870a30cb20aaa0335d83314fe0aa7e63a862648041a72a6321523220b1ace9bb701b21ac1253cb812c15575a9085eabeade73a4ae76e6a7b158a20586d78a5ac620a5c9abcc9c043350a73656b0abe822da5e0ba76045fad75401d7a3b703791b7e99261710f86b72421d240a347638377205a152c794130a4e047742b888303bddc309116764de7424cebea6db65348ac537e01a9cc56ea667d5aa87ac9aaa4317d262c10143050b8d07a728ca633c13e468abcead372c77b8ecf3b986b98c1e55860b2b4216766ad874c35ed7205068739230220b5a2317d102c598356f168acbe80608de4c9a710b8dd07078cd7c671058af1b0b8304a314f7b29be78a933c7b9294424954a1bf8bc745de86198659e0e1225a910726074969c39a97c19240601a46e013dcdcb677a8cbd2c95a40629c256f24a328951df57502ab30772cc7e5b850027c8551781ce4985bdacf6b865c104e8a4bc65c41694d456b7169e45ab3d7acabeafe23ad6a7b94d1979a2f4c1cae7cd77d681d290b5d8e451bfdcccf5310b9d12a88ec29b10255d5e17a192670aa9731c5ca67ec784c502781be8527d6fc003c6701b3632284b40307a527c7620377feb0b73f722c9e3cd4dec64876b93ab5b7cfc4a657f852b659282864384f442b22e8a21109387b8b47585fc680d0ba45c7a8b1d7274bda57845d100d0f42a3b74628773351fd7ac305b2497639be90b3f4f71a6aa3561eecc6a691bb5cb3914d8634ca1e1af543c049a8c6e868c51f0423bd2d5ae09b79e57c27f3fe3ae2b26a441babfc6718ce8c05b4fe793b910b8fbcbbe7f1013242b40e0514d0bdc5c88bac594c794ce5122fbf34896819147b928381587963b0b90034aa07a10be176e01c80ad6a4b71b10af4241400a2a4cbbc05961a15ec1474ed51a3cc6d35800679a462809caa3ab4f7094cd6610b4a700cba939e7eac93e38c99755908727619ed76a34e53c4fa25bfc97008206697dd145e5b9188e5b014e941681e15fe3e132b8a3903474148ba28b987111c9bcb3989bbbc671c581b44a492845f288e62196e471fed3c39c1bbddb0837d0d4706b0922c4'), + m=unhex('2ce74ad291133518fe60c7df5d251b9d82add48462ff505c6e547e949e6b6bf7'), + c_expected=unhex('56b42d593aab8e8773bd92d76eabddf3b1546f8326f57a7b773764b6c0dd30470f68dff82e0dca92509274ecfe83a954735fde6e14676daaa3680c30d524f4efa79ed6a1f9ed7e1c00560e8683538c3105ab931be0d2b249b38cb9b13af5ceaf7887a59dba16688a7f28de0b14d19f391eb41832a56479416ccf94e997390ed7878eeaff49328a70e0ab5fce6c63c09b35f4e45994de615b88bb722f70e87d2bbd72ae71e1ee9008e459d8e743039a8ddeb874fce5301a2f8c0ee8c2fee7a4ee68b5ed6a6d9ab74f98bb3ba0fe89e82bd5a525c5e8790f818ccc605877d46c8bdb5c337b025bb840ff471896e43bfa99d73dbe31805c27a43e57f0618b3ae522a4644e0d4e4c1c548489431be558f3bfc50e16617e110dd7af9a6fd83e3fbb68c304d15f6cb700d61d7aa915a6751ea3ba80223e654132a20999a43bf408592730b9a9499636c09fa729f9cb1f9d3442f47357a2b9cf15d3103b9bf396c23088f118ede346b5c03891cfa5d517cef8471322e7e31087c4b036abad784bff72a9b11fa198facbcb91f067feaf76fcfe5327c1070b3da6988400756760d2d1f060298f1683d51e3616e98c51c9c03aa42f2e633651a47ad3cc2ab4a852ae0c4b04b4e1c3dd944445a2b12b4f42a6435105c04122fc3587afe409a00b308d63c5dd8163654504eedbb7b5329577c35fbeb3f463872cac28142b3c12a740ec6ea7ce9ad78c6fc8fe1b4df5fc55c1667f31f2312da07799dc870a478608549fedafe021f1cf2984180364e90ad98d845652aa3cdd7a8eb09f5e51423fab42a7b7bb4d514864be8d71297e9c3b17a993f0ae62e8ef52637bd1b885bd9b6ab727854d703d8dc478f96cb81fce4c60383ac01fcf0f971d4c8f352b7a82e218652f2c106ca92ae686bacfcef5d327347a97a9b375d67341552bc2c538778e0f9801823ccdfcd1eaaded55b18c9757e3f212b2889d3857db51f981d16185fd0f900853a75005e3020a8b95b7d8f2f2631c70d78a957c7a62e1b3719070acd1fd480c25b83847da027b6ebbc2eec2df22c87f9b46d5d7baf156b53cee929572b92c4784c4e829f3446a1ffe47f99decd0436029ddebd3ed8e87e5e73d123dbe8a4ddacf2abde87f33ae2b621c0ec5d5cad1259deec2aeff6088f04f27a20338b5762543e5100899a4cbfb7b3ca456b3a19b83a4c432230c23e1c7f107c4cb112152f1c0f30da0bb33f4f11f47eea43872bafa84ae22256d708e0604dade4b2a4dde8cccf11930e13553934ae3ece52f3d7ccc00287377879fe6b8ece7ef79423507c9da339559c20de1c51955999bae47401dc3cdfaa1b256d09c7db9fc8698bfcefa7302d56fbcde1fbaaa1c653454e6fd3d84e4f79a931c681cbb6cb462b10dae112bdfb7f65c7fdf6e5fc594ec3a474a94bd97e6ec81f71c230bf70ca0f13ce3dffbd9ff9804efd8f37a4d3629b43a8f55544ebc5ac0abd9a33d79699068346a0f1a3a96e115a5d80be165b562d082984d5aacc3a2301981a6418f8ba7d7b0d7ca5875c6'), + k_expected=unhex('2696d28e9c61c2a01ce9b1608dcb9d292785a0cd58efb7fe13b1de95f0db55b3')) + # Encaps test from test group 3, test case 51 + encaps_test('mlkem1024', + ek=unhex('307a4cea4148219b958ea0b7886659235a4d1980b192610847d86ef32739f94c3b446c4d81d89b8b422a9d079c88b11acaf321b014294e18b296e52f3f744cf9634a4fb01db0d99ef20a633a552e76a0585c6109f018768b763af3678b4780089c1342b96907a29a1c11521c744c2797d0bf2b9ccdca614672b45076773f458a31ef869be1eb2efeb50d0e37495dc5ca55e07528934f6293c4168027d0e53d07facc6630cb08197e53fb193a171135dc8ad9979402a71b6926bcdcdc47b93401910a5fcc1a813b682b09ba7a72d2486d6c799516465c14729b26949b0b7cbc7c640f267fed80b162c51fd8e09227c101d505a8fae8a2d7054e28a78ba8750decf9057c83979f7abb084945648006c5b28804f34e73b238111a65a1f500b1cc606a848f2859070beba7573179f36149cf5801bf89a1c38cc278415528d03bdb943f96280c8cc52042d9b91faa9d6ea7bcbb7ab1897a3266966f78393426c76d8a49578b98b159ebb46ee0a883a270d8057cd0231c86906a91dbbade6b2469581e2bca2fea8389f7c74bcd70961ea5b934fbcf9a6590bf86b8db548854d9a3fb30110433bd7a1b659ca8568085639237b3bdc37b7fa716d482a25b54106b3a8f54d3aa99b5123da96066904592f3a54ee23a7981ab608a2f4413cc658946c6d7780ea765644b3cc06c70034ab4eb351912e7715b56755d09021571bf340ab92598a24e811893195b96a1629f8041f58658431561fc0ab15292b913ec473f04479bc145cd4c563a286235646cd305a9be1014e2c7b130c33eb77cc4a0d9786bd6bc2a954bf3005778f8917ce13789bbb962807858b67731572b6d3c9b4b5206fac9a7c8961698d88324a915186899b29923f08442a3d386bd416bcc9a100164c930ec35eafb6ab35851b6c8ce6377366a175f3d75298c518d44898933f53dee617145093379c4659f68583b2b28122666bec57838991ff16c368dd22c36e780c91a3582e25e19794c6bf2ab42458a8dd7705de2c2aa20c054e84b3ef35032798626c248263253a71a11943571340a978cd0a602e47dee540a8814ba06f31414797cdf6049582361bbaba387a83d89913fe4c0c112b95621a4bda8123a14d1a842fb57b83a4fbaf33a8e552238a596aae7a150d75da648bc44644977ba1f87a4c68a8c4bd245b7d00721f7d64e822b085b901312ec37a8169802160cce1160f010be8cbcace8e7b005d7839234a707868309d03784b4273b1c8a160133ed298184704625f29cfa086d13263ee5899123c596ba788e5c54a8e9ba829b8a9d904bc4bc0bbea76bc53ff811214598472c9c202b73eff035dc09703af7bf1babaac73193cb46117a7c9492a43fc95789a924c5912787b2e2090ebbcfd3796221f06debf9cf70e056b8b9161d6347f47335f3e1776da4bb87c15cc826146ff0249a413b45aa93a805196ea453114b524e310aedaa46e3b99642368782566d049a726d6cca910993aed621d0149ea588a9abd909dbb69aa22829d9b83ada2209a6c2659f2169d668b9314842c6e22a74958b4c25bbdcd293d99cb609d866749a485dfb56024883cf5465dba0363206587f45597f89002fb8607232138e03b2a894525f265370054b48863614472b95d0a2303442e378b0dd1c75acbab971a9a8d1281c79613acec6933c377b3c578c2a61a1ec181b101297a37cc5197b2942f6a0e4704c0ec63540481b9f159dc255b59bb55df496ae54217b7689bd51dba0383a3d72d852ffca76df05b66eeccbd47bc53040817628c71e361d6af889084916b408a466c96e7086c4a60a10fcf7537bb94afbcc7d437590919c28650c4f2368259226a9bfda3a3a0ba1b5087d9d76442fd786c6f81c68c0360d7194d7072c4533aea86c2d1f8c0a27696066f6cfd11003f797270b32389713cffa093d991b63844c385e72277f166f5a3934d6bb89a4788de28321defc7457ab484bd30986dc1dab3008cd7b22f69702fabb9a1045407da4791c3590ff599d81d688cfa7cc12a68c50f51a1009411b44850f9015dc84a93b17c7a207552c661ea9838e31b95ead546248e56be7a5130505268771199880a141771a9e47acfed590cb3aa7cb7c5f74911d8912c29d6233f4d53bc64139e2f55be75507dd77868e384aec581f3f411db1a742972d3ebfd3315c84a5ad63a0e75c8bca3e3041e05d9067aff3b1244f763e7983'), + m=unhex('59c5154c04ae43aaff32700f081700389d54bec4c37c088b1c53f66212b12c72'), + c_expected=unhex('e2d5fd4c13cea0b52d874fea9012f3a51743a1093710bbf23950f9147a472ee5533928a2f46d592f35da8b4f758c893b0d7b98948be447b17cb2ae58af8a489ddd9232b99b1c0d2de77caa472bc3bbd4a7c60dbfdca92ebf3a1ce1c22dad13e887004e2924fd22656f5e508791de06d85e1a1426808ed9a89f6e2fd3c245d4758b22b02cade33b60fc889a33fc4447edebbfd4530de86596a33789d5dba6e6ec9f89879af4be4909a69017c9bb7a5e31815ea5f132eec4984faa7ccf594dd00d4d8487e45621af8f6e330551439c93ec078a7a3cc1594af91f8417375fd6088ceb5e85c67099091bac11498a0d711455f5e0d95cd7bbe5cdd8fecb319e6853c23c9be2c763df578666c40a40a87486e46ba8716146192904510a6dc59da8025825283d684db91410b4f12c6d8fbd0add75d3098918cb04ac7bc4db0d6bcdf1194dd86292e05b7b8630625b589cc509d215bbd06a2e7c66f424cdf8c40ac6c1e5ae6c964b7d9e92f95fc5c8852281628b81b9afabc7f03be3f62e8047bb88d01c68687b8dd4fe63820062b6788a53729053826ed3b7c7ef8241e19c85117b3c5341881d4f299e50374c8eefd5560bd18319a7963a3d02f0fbe84bc484b5a4018b97d274191c95f702bab9b0d105faf9fdcff97e437236567599faf73b075d406104d403cdf81224da590bec2897e30109e1f2e5ae4610c809a73f638c84210b3447a7c8b6dddb5ae200bf20e2fe4d4ba6c6b12767fb8760f66c5118e7a9935b41c9a471a1d3237688c1e618cc3be936aa3f5e44e086820b810e063211fc21c4044b3ac4d00df1bcc7b24dc07ba48b23b0fc12a3ed3d0a5cf7671415ab9cf21286fe63fb41418570555d4739b88104a8593f293025a4e3ee7c67e4b48e40f6ba8c09860c3fbbe55d45b45fc9ab629b17c276c9c9e2af3a043beafc18fd4f25ee7f83bddcd2d93914b7ed4f7c9af127f3f15c277be16551fef3ae03d7b9143f0c9c019ab97eea076366131f518363711b34e96d3f8a513f3e20b1d452c4b7ae3b975ea94d880dac6693399750d02220403f0d3e3fc1172a4de9dc280eaf0fee2883a6660bf5a3d246ff41d21b36ea521cf7aa689f800d0f86f4fa1057d8a13f9da8fffd0dc1fad3c04bb1cccb7c834db051a7ac2e4c60301996c93071ea416b421759935659cf62ca5f13ae07c3b195c148159d8beb03d440b00f5305765f20c0c46eee59c6d16206402db1c715e888bde59c781f35a7cc7c1c5ecb2155ae3e959c0964cc1ef8d7c69d1458a9a42f95f4c6b5b996345712aa290fbbf7dfd4a6e86463022a3f4725f6511bf7ea5e95c707cd3573609aadeaf540152c495f37fe6ec8bb9fa2aa61d15735934f4737928fde90ba995722465d4a64505a5201f07aa58cfd8ae226e02070b2dbf512b975319a7e8753b4fdae0eb4922869cc8e25c4a5560c2a0685de3ac392a8925ba882004894742e43ccfc277439ec8050a9aeb42932e01c840dfcedcc34d3991289a62c17d1284c839514b93351dbb2dda81f924565d70e7079d5b8126caab7a4a1c731655a53bcc09f5d63ec9086dea650055985edfa8297d9c95410c5d1894d17d5930549adbc2b8733c99fe62e17c4de34a5d89b12d18e42a422d2ce779c2c28eb2d98003d5cd323fcbecf02b5066e0e734810f09ed89013c00f011bd220f2e5d6a362df90599198a093b03c8d8efbfe0b617592faf1e64220c4440b53ffb47164f369c95290ba9f3108d686c57db645c53c012e57af25bd6693e2cc6b57651af1591fe5d8916640ec017c253df0606bb6b3035fae748f3d4034223b1b5efbf5283e778c1094291cf7b19be0f317350e6f8518fde0efb1381fb6e16c241f7f17a5210693a274159e7fac868cd0dc4359c3d9eefea0d9e31e43fa651392c65a543a59b3eee3a639dc9417d056a5ff0f160beee2eac29a7d88c0982cf70b5a46379f21e506aac61a9bb1b8c2b9dab0e44a823b61d0aa11d94f76a4a8e21f9d4280683208f4ea911116f6fd6a97426934ec3426b8c8f703da85e9dcf99336136003728b8ecdd04a389f6a817a78bfa61ba46020bf3c34829508f9d06d1553cd987aac380d86f168843ba3904de5f7058a41b4cd388bc9ce3aba7ee7139b7fc9e5b8cfaaa38990bd4a5db32e2613e7ec4f5f8b1292a38c6f4ff5a40490d76b126652fcf86e245235d636c65cd102b01e22781a72918c'), + k_expected=unhex('7264bde5c6cec14849693e2c3c86e48f80958a4f6186fc69333a4148e6e497f3')) + # Decaps test from test group 1, test case 1 (accept) + decaps_test('mlkem512', + dk=unhex('a5e26e1b2360203944acfc2d7c376780e55b5a5ca38674919437c794f54b8217bb0629c84c692ef7827eed864d0c508990ca4553f16f4720cb75368c1b8ca9dbc175f51bbebaa456f36611a2364775d248c0f4c40b342608f7370a983cf75c915570248e367375b665d9357ce4a8553e659be4a60ca68b58724689c23b74d34c9e78e168e7cb0df84641e41b6e6807be6cf4cf8f338525d57090b08aab5721216395c49147f6e817b117b129987317a7a5ff15a279f86af93c6a4995954000c3d4d8b0a07499a95a5c98d0b8303702dfd801b67c37268904c96abc462750384baea767a5ad30c5d452682b3ac864d1671db38f1cf2ce6e6c901d39c144da3d93b863f95717c3c585ab876d3ef2b10afa0b8142164c3c27fb179a923a3f924b15cebb22ec762907324f1cd4c47573ca1f103ca88844f3b86687280b3b5bb569b1c118b63565055834f39f320cb88c05c199e29684d7802cf45d8da342cc444d91a84d6d9461c873b66f9785488723a167412019077c9a7fcf4c7bd028be3007b3483026a442a095124c9607c950443fd69993615697e9ac1cb9d380437b85eb300ce4d9b5a5bc2132660da3527031a1057a565f2c76775565b0088637707410f2e955355425efe496113149cf52c901bccc48864c8aa4262367213602b63aa1a8bed77826c0c476152ab3464a20c9cd73f17a1d019466f2ae37859e6e5a8bb8862a480c1b12d6797b79663ed2333f188f34e6cf6ec87e43979f88787ce35877ddf0b689547bf5ba9eebb2659d76354ebc39ee83975310aca4f8867ff290793cc08bf29e60a97c28a71ea3084fe27845ab3664e80592412043b03056fdd5744bd74c9584094c2b75c689aca8e4b3d3f91994e4722b9b331399310975275a0065935b6cdf5a6a8216188452394238bc82736488a84a0c96c580a81c69032ad5e96f4c3061df5ab246c258cba0b68a32916bfc6686730b3ff0944a070f535a113fc349cddb0b67b40debfb5215167090f9891365bb3d87639fda05843a079a430fd5892f57ac4510450dec00b7905a3a14442231919f9ed4a76b2b159a6ccc3685b3dd1924935aa8e617af18b5a065ac45727767ee897cf4f9442b2ace30c0237b307d3e76bf8eeb78addc4aacd16463d8602fd5487b63c88bb66027f37d0d614d6f9c24603c42947664ac4398c6c52383469b4f9777e5ec7206210f3e5a796bf45c53268e25f39ac261af3bfa2ee755beb8b67ab3ac8df6c629c1176e9e3b965e9369f9b3b92ad7c20955641d99526fe7b9fe8c850820275cd964849250090733ce124ecf316624374bd18b7c358c06e9c136ee1259a9245abc55b964d689f5a08292d28265658ebb40cbfe488a2228275590ab9f32a34109709c1c291d4a23337274c7a5a5991c7a87b81c974ab18ce77859e4995e7c14f0371748b7712fb52c5966cd63063c4f3b81b47c45dde83fb3a2724029b10b3230214c04fa0577fc29ac9086ae18c53b3ed44e507412fca04b4f538a51588ec1f1029d152d9ae7735f76a077aa9484380aed9189e5912487fcc5b7c7012d9223dd967eecdac3008a8931b648243537f548c171698c5b381d846a72e5c92d4226c5a8909884f1c4a3404c1720a5279414d7f27b2b982652b6740219c56d217780d7a5e5ba59836349f726881dea18ef75c0772a8b922766953718cacc14ccbacb5fc412a2d0be521817645ab2bf6a4785e92bc94caf477a967876796c0a5190315ac0885671a4c749564c3b2c7aed9064eba299ef214ba2f40493667c8bd032aec5621711b41a3852c5c2bab4a349ce4b7f085a812bbbc820b81befe63a05b8bcdfe9c2a70a8b1aca9bf9816481907ff4432461111287303f0bd817c05726bfa18a2e24c7724921028032f622bd960a317d83b356b57f4a8004499cbc73c97d1eb7745972631c0561c1a3ab6ef91bd363280a10545da693e6d58aed6845e7cc5f0d08ca7905052c77366d1972ccfcc1a27610cb543665aa798e20940128b9567a7edb7a900407c70d359438435e13961608d552a94c5cda7859220509b483c5c52a210e9c812bc0c2328ca00e789a56b2606b90292e3543dacaa2431841d61a22ca90c1ccf0b5b4e0a6f640536d1a26ab5b8d2151327928ce02904cf1d15e32788a95f62d3c270b6fa1508f97b9155a2726d80a1afa3c5387a276a4d031a08abf4f2e74f1a0bb8a0fd3cb0ac923a76d541ca65fdec9c788a407326c7db508119f617f43b6e8a6f48a398702e051c20c31de77a1ba6777829f5539c886e3e14ded294d56ae5e88ac06ab09'), + c=unhex('19c592505907c24c5fa2ebfa932d2cbb48f3e4340a28f7eba5d068fcacabedf77784e2b24d7961775f0bf1a997ae8ba9fc4311be63716779c2b788f812cbb78c74e7517e22e910eff5f38d44469c50de1675ae198fd6a289ae7e6c30a9d4351b3d1f4c36eff9c68da91c40b82dc9b2799a33a26b60a4e70d7101862779469f3a9daec8e3e8f8c6a16bf092fba5866186b8d208fdeb274ac1f829659dc2be4ac4f306cb5584bad1936a92c9b76819234281bb395841c25756086ea564ca3e227e3d9f1052c0766d2eb79a47c150721e0dea7c0069d551b264801b7727ecaf82eecb99a876fda090bf6c3fc6b109f1701485f03ce66274b8435b0a014cfb3e79cced67057b5ae2ad7f5279eb714942e4c1ccff7e85c0db43e5d41289207363b444bb51bb8ab0371e70cbd55f0f3dad403e105176e3e8a225d84ac8bee38c821ee0f547431145dcb3139286abb11794a43a3c1b5229e4bcfe959c78adaee2d5f2497b5d24bc21fa03a9a58c2455373ec89583e7e588d7fe67991ee93783ed4a6f9eeae04e64e2e1e0e699f6dc9c5d39ef9278c985e7fdf2a764ffd1a0b95792ad681e930d76df4efe5d65dbbd0f1438481ed833ad4946ad1c69ad21dd7c86185774426f3fcf53b52ad4b40d228ce124072f592c7daa057f17d790a5bd5b93834d58c08c88dc8f0ef488156425b744654eaca9d64858a4d6ceb478795194bfadb18dc0ea054f9771215ad3cb1fd031d7be4598621926478d375a1845aa91d7c733f8f0e188c83896edf83b8646c99e29c0da2290e71c3d2e970720c97b5b7f950486033c6a2571ddf2bccdabb2dfa5fce4c3a1884606041d181c728794ae0e806ecb49af16756a4ce73c87bd4234e60f05535fa5929fd5a34473266401f63bbd6b90e003472ac0ce88f1b666597279d056a632c8d6b790fd411767848a69e37a8a839bc766a02ca2f695ec63f056a4e2a114cacf9fd90d730c970db387f6de73395f701a1d953b2a89dd7edad439fc205a54a481e889b098d5255670f026b4a2bf02d2bdde87c766b25fc5e0fd453757e756d18c8cd912f9a77f8e6bf0205374b462'), + k_expected=unhex('0bf323338d6f0a21d5514b673cd10b714ce6e36f35bcd1bf544196368ee51a13')) + # Decaps test from test group 2, test case 26 (accept) + decaps_test('mlkem768', + dk=unhex('b09125afb3cfb5295581373ab6885284d9706318280d223edc987fd14410dbe82e6ac89adfab70e67ca4b1c641ad037fd8c47870f159ec79cdcd52605b9890499bb6dbd8347f342c61436b642c0ddf4617db06198b8285dce4c09d9775a2f41c8cd18af8e75f57d4127df94d901ac83bacbd584cc50c43750f49b357f59350875c9b475480a8aaa168592ddb158614a639813566d205368c6c39f0413ca3230df60d44008282b682ac66b76c3c95f00b2a555035529c86ef3905b4a3968fea7802b6c5eecb08e8f0c42d7ab7cd21a62fb136412a1840b52c99970ccf51892f73497c3775be2189f7fc25e7c74d81fc217683292aa4866ddb04469855323a0810f0893de5c7f94a9c0b5337db83c44891b2e694695b76575032bf51761682958bd4f97be9a355b4a85bb6858b7e5a5ef653ab781056af9187d811c3a8936e5706503db57062410bcc9421f1ab867a657856c411c4e025ecb3c387729ae8e112f330b988e22f47c35c280750d21b107687af7b329ef3cb5289f06fb7d44548391e97ba6dd499b5907c54958413d92aa99d5646cf47a8f48cb70a07ad056b4eefe6c8c46645f7028a32410558638c48e83ac1570160c3833bf64052f5b7df4364d3e0b24e790aa7c98cee0441e6731d9de22d156c61e1c740397672ef54724f01b9d49923aa321f86b98823f21360138392b90c69434635275f9bfbb9b8a99e8e1b7f4ec25f75dbce33c13f750170bd6722efe496e7463e16aaa5867b869a96ad41b22bd2556c924596fd778d79a102f6e46d8eb18fefac8db19993e5414ac816705286892492c8c9e852d6145dff0c10e4a6703a459e7e732a6dfa2766a622b0622bfedb8f41c125f61b2ec264853b9ccc165979f6a263beb148905aac7618a70e829e23f28696f92ef6fa07c102cdbdb1288ba5cff3a81abba15974535fe3106a80068f14e98964572350a7112b1601c196710c096ccf164fbce1aabac9c5b9535070e61ab8068d611ca765fabb6412607dab30c4fc6ad073731fdc4c48b88e267c47b439ad2560c30561815ceb1f52c896489944bbbab52b1b1d1680a1057964dafa600c93a39a447ddbb0adf911afe3e823d8acc7cc04659f625f2c1837bb175282542cd22601f621581ab5a6c0384e087ccd32a5380b522fdd3a4202b5b41c85caff2903b2dc2645703d9bc711fbb404c0c0376187ac588aaf5718522d2273a9408dabcbc9701698d2da172aa6267a4c9693a24011c2265a2b6dc8e96304a98ddc5319a3140c399a08412c20f48537870bb84c32a094457895511ff7ec421de01a64b78534653f78327441b90cd115939dfaafa95b40d0a63d62d12eb5c9096018cc83871e44e6cd0be26d16b7b5a209b8e6471d2954adf9fabd0153707c9caa2bcc38ded841c791a0eb597eeee2c518d926edb28ab53caa5b7746466931b0ac9150688bf37049c1f82bcf648332434cd0a92fd2c958353a26cb65cb499057109b2d688cc43c4b385da7c50868af1b8075e57088f5db12dfa493eacb6dc4ec6e205baa2a89858ec2823c00553714cde47a96e36c7c198b3ec57ccf74d92cddb86aa0a8b8b5ca9d52bb60aba79f4f72b0125532ceb7a9077480d2bb60df51a989d2cb65f94dcbfc890efc7d0e5a7a38344d1641a3d0b024d50797a5f23c3a18b3101a1269069f43a842bacc098a8821271c673db1beb33034e4d7774d16635c7c2c3c2763453538bc1632e1851591a51642974e5928abb8e55fe55612f9b141aff015545394b2092e590970ec29a7b7e7aa1fb4493bf7cb731906c2a5cb49e6614859064e19b8fa26af51c44b5e7535bfdac072b646d3ea490d277f0d97ced47395fed91e8f2bce0e3ca122c2025f74067ab928a822b35653a74f06757629afb1a1caf237100ea935e793c8f58a71b3d6ae2c8658b10150d4a38f572a0d49d28ae89451d338326fdb3b4350036c1081117740edb86b12081c5c1223dbb5660d5b3cb3787d481849304c68be875466f14ee5495c2bd795ae412d09002d65b8719b90cba3603ac4958ea03cc138c86f7851593125334701b677f82f4952a4c93b5b4c134bb42a857fd15c650864a6aa94eb691c0b691be4684c1f5b7490467fc01b1d1fda4dda35c4ecc231bc73a6fef42c99d34eb82a4d014987b3e386910c62679a118f3c5bd9f467e4162042424357db92ef484a4a1798c1257e870a30cb20aaa0335d83314fe0aa7e63a862648041a72a6321523220b1ace9bb701b21ac1253cb812c15575a9085eabeade73a4ae76e6a7b158a20586d78a5ac620a5c9abcc9c043350a73656b0abe822da5e0ba76045fad75401d7a3b703791b7e99261710f86b72421d240a347638377205a152c794130a4e047742b888303bddc309116764de7424cebea6db65348ac537e01a9cc56ea667d5aa87ac9aaa4317d262c10143050b8d07a728ca633c13e468abcead372c77b8ecf3b986b98c1e55860b2b4216766ad874c35ed7205068739230220b5a2317d102c598356f168acbe80608de4c9a710b8dd07078cd7c671058af1b0b8304a314f7b29be78a933c7b9294424954a1bf8bc745de86198659e0e1225a910726074969c39a97c19240601a46e013dcdcb677a8cbd2c95a40629c256f24a328951df57502ab30772cc7e5b850027c8551781ce4985bdacf6b865c104e8a4bc65c41694d456b7169e45ab3d7acabeafe23ad6a7b94d1979a2f4c1cae7cd77d681d290b5d8e451bfdcccf5310b9d12a88ec29b10255d5e17a192670aa9731c5ca67ec784c502781be8527d6fc003c6701b3632284b40307a527c7620377feb0b73f722c9e3cd4dec64876b93ab5b7cfc4a657f852b659282864384f442b22e8a21109387b8b47585fc680d0ba45c7a8b1d7274bda57845d100d0f42a3b74628773351fd7ac305b2497639be90b3f4f71a6aa3561eecc6a691bb5cb3914d8634ca1e1af543c049a8c6e868c51f0423bd2d5ae09b79e57c27f3fe3ae2b26a441babfc6718ce8c05b4fe793b910b8fbcbbe7f1013242b40e0514d0bdc5c88bac594c794ce5122fbf34896819147b928381587963b0b90034aa07a10be176e01c80ad6a4b71b10af4241400a2a4cbbc05961a15ec1474ed51a3cc6d35800679a462809caa3ab4f7094cd6610b4a700cba939e7eac93e38c99755908727619ed76a34e53c4fa25bfc97008206697dd145e5b9188e5b014e941681e15fe3e132b8a3903474148ba28b987111c9bcb3989bbbc671c581b44a492845f288e62196e471fed3c39c1bbddb0837d0d4706b0922c472e31df613da9a1dd33b5d2d8939684b89f7649e1c59b959ffbe972786c477f66177dbf3b059173fd06afcd90e80e862174fc57f97607bbff5b73d6360fb5c37'), + c=unhex('56b42d593aab8e8773bd92d76eabddf3b1546f8326f57a7b773764b6c0dd30470f68dff82e0dca92509274ecfe83a954735fde6e14676daaa3680c30d524f4efa79ed6a1f9ed7e1c00560e8683538c3105ab931be0d2b249b38cb9b13af5ceaf7887a59dba16688a7f28de0b14d19f391eb41832a56479416ccf94e997390ed7878eeaff49328a70e0ab5fce6c63c09b35f4e45994de615b88bb722f70e87d2bbd72ae71e1ee9008e459d8e743039a8ddeb874fce5301a2f8c0ee8c2fee7a4ee68b5ed6a6d9ab74f98bb3ba0fe89e82bd5a525c5e8790f818ccc605877d46c8bdb5c337b025bb840ff471896e43bfa99d73dbe31805c27a43e57f0618b3ae522a4644e0d4e4c1c548489431be558f3bfc50e16617e110dd7af9a6fd83e3fbb68c304d15f6cb700d61d7aa915a6751ea3ba80223e654132a20999a43bf408592730b9a9499636c09fa729f9cb1f9d3442f47357a2b9cf15d3103b9bf396c23088f118ede346b5c03891cfa5d517cef8471322e7e31087c4b036abad784bff72a9b11fa198facbcb91f067feaf76fcfe5327c1070b3da6988400756760d2d1f060298f1683d51e3616e98c51c9c03aa42f2e633651a47ad3cc2ab4a852ae0c4b04b4e1c3dd944445a2b12b4f42a6435105c04122fc3587afe409a00b308d63c5dd8163654504eedbb7b5329577c35fbeb3f463872cac28142b3c12a740ec6ea7ce9ad78c6fc8fe1b4df5fc55c1667f31f2312da07799dc870a478608549fedafe021f1cf2984180364e90ad98d845652aa3cdd7a8eb09f5e51423fab42a7b7bb4d514864be8d71297e9c3b17a993f0ae62e8ef52637bd1b885bd9b6ab727854d703d8dc478f96cb81fce4c60383ac01fcf0f971d4c8f352b7a82e218652f2c106ca92ae686bacfcef5d327347a97a9b375d67341552bc2c538778e0f9801823ccdfcd1eaaded55b18c9757e3f212b2889d3857db51f981d16185fd0f900853a75005e3020a8b95b7d8f2f2631c70d78a957c7a62e1b3719070acd1fd480c25b83847da027b6ebbc2eec2df22c87f9b46d5d7baf156b53cee929572b92c4784c4e829f3446a1ffe47f99decd0436029ddebd3ed8e87e5e73d123dbe8a4ddacf2abde87f33ae2b621c0ec5d5cad1259deec2aeff6088f04f27a20338b5762543e5100899a4cbfb7b3ca456b3a19b83a4c432230c23e1c7f107c4cb112152f1c0f30da0bb33f4f11f47eea43872bafa84ae22256d708e0604dade4b2a4dde8cccf11930e13553934ae3ece52f3d7ccc00287377879fe6b8ece7ef79423507c9da339559c20de1c51955999bae47401dc3cdfaa1b256d09c7db9fc8698bfcefa7302d56fbcde1fbaaa1c653454e6fd3d84e4f79a931c681cbb6cb462b10dae112bdfb7f65c7fdf6e5fc594ec3a474a94bd97e6ec81f71c230bf70ca0f13ce3dffbd9ff9804efd8f37a4d3629b43a8f55544ebc5ac0abd9a33d79699068346a0f1a3a96e115a5d80be165b562d082984d5aacc3a2301981a6418f8ba7d7b0d7ca5875c6'), + k_expected=unhex('2696d28e9c61c2a01ce9b1608dcb9d292785a0cd58efb7fe13b1de95f0db55b3')) + # Decaps test from test group 3, test case 51 (accept) + decaps_test('mlkem1024', + dk=unhex('673751cbb596541131c66398662cb4b0eb80796a88b28144a5bbc854f80d4b35be0ab241e4795f8fbba814f50fa80498cbe8bf68a0a583a4c5981b41df0667db614a628c3060697438e62c8d36026ee29c96b673bf1a194ee49481351f4d1748dd01cd023142f01057142b741cba8302e432f88c63d0b4b5767ac3a5a59afa3a321e65b1d1511807a06e16a04b2f1070e465586d4a9b68e2b42d57a356fa7bb3d04e51b193ff4c757cfa0f15924ea6e49afb83b2919c985869ada544338f44ae96a874c425af87bc73f3cb0fd2627b1539b1f19a77e36b7fc817851d39bd8a069a6c2202c17469d421a588e65daf450030b6674ec1c734aa25414b119e61b26efc90df81059d2b9599414f93692bf45a4b1c5cc09edb37b1b1433026aea6b0200722b819c7bc061c53a4304992fca2aee2324a324ab91c3e5d562096b8a141756940f15a2800c274ea4f65817e639c5d2a278c6a294f9db331f84ccb0a10309f530a06eb962573c86005c15bfc7531a143026396721297e25cb655a294964b2fe531905f2802376b8ace35ae3e2814bab7062bc1a840657dbfcb5f41bb55475697849a31e2222e995518ca7640ad4b9cee9820984138be0510ffd6ac225393a5f0cb030528cd2a0610e78a5cf1b073039a6d143068c53dbd15a1d4446da7b310ee795d1fb31b2f97008f83bdf348a593a3bdcbb571907b36d0978162c253e6f50106c463149834abfb0707d8ab4a4babc323598a085b309764b7c32c9db0c9f2d52ef2f00bace7846868c33b82afa430a4c2f67b698a60526a161cd62115dca767c203e3e2cc787031a73b5b7dba1eee5ab04b77bb569b952d9a15d198779804197d23c18e5b055f5c8087d742f64418d6505e70418abfc6b1bf7bb3de286599f4676cf87946d65144998afae1c689449e3f349fd0809afb856dde4a94a2c0258d56432f40c3da812d3fd3b72259a61d2882e0f50b355121e564c6bd33366f32bf4a5996b9998961354925a2bacdf48056118453ac3792a7879b71579adb65f5d83b1ed6c8c49836de379daa027e62b96f683c1688935cb3fccd64329267273e60c6cd59ba1b7fc911e2662527eccb7a474e5ef00ca9f789a3838e889242e7fb2b08f3790613c4eed3c912ec4eb029b971096b384727697b4ddc3b698c9a6da6971fa4c574ecd18eb1c84c0c5790153aa6b9db61d8bac0a680a37ed623582a7e8c0885ebb35af341477764368e0647b14553672316d0b90317c5b53aa747e61b4750db9e63cc3712900005ca24226b523e0a179582c85968c107857bb41521b7342b13dcac462a53be38446f2142519667b48b1c68fcafa4d3c7e3e5aff163c41f2c1b4dbac5456c30776078e7c3a713819f6b9aca55d77d60637183a723035730f94285c42ac3587637f66ac30f2c4039e60420967576e27b96c8c004d9585f33939ac44f0d195b35d472fc219076f12d0984ac844728d5d2266bb5cd8b325dda497b4f397bfe722c9d7684201a921f502271985cb3f31c04884c090b063631253dc454537031f2c82c10a1722de6c556464dc9d64389da37e469480c921065c79a30c83c867c952b30548a6b5bdfeb6ea6247480f163b427b17cf94889220fe934564dab90f5b6a11648870b654495a6691ae21fea86bdc8c49093fa07e926af3aba0e7cec21f613b49986c6c8a139eda70b7ed8211a3215e8c43ef8c151ae61740ef83b48276033614b58e9ceb992233cd21dff70c7a6f7171707a2add37acbf136a4eb4a79517fd0c8aff0b5126435c3100331f208a546c9a4044a8f0503c8ade9506a018b4ca7c6e8d70120017d38b13b52786a85a540d81b8e71c376b796a7215abf065086d3c80ee94b8f09e2a3ba13b82583b825388e87ba010af507173563789a1dcd088907c52bd7fc1c6930605f060f37978211c10fb5717e3fa291d20b5d43fb74cd4711394b0027e41c52b523797470532cbe123c92950720e5e255256577d4e156ebd4c698d813405c61430b978694acde78031e74ba1d8517dae2346f008411231fcce7bff75bc361e691e776049004097b36490d876288701b2d3a1743ab8753d47ac6200e2da7458d3a059681233872794e6720186b20108b1d1033971ce19ed67a2a28e499a360a4ad86ae4194034f202f8fa3626fe75f307a4cea4148219b958ea0b7886659235a4d1980b192610847d86ef32739f94c3b446c4d81d89b8b422a9d079c88b11acaf321b014294e18b296e52f3f744cf9634a4fb01db0d99ef20a633a552e76a0585c6109f018768b763af3678b4780089c1342b96907a29a1c11521c744c2797d0bf2b9ccdca614672b45076773f458a31ef869be1eb2efeb50d0e37495dc5ca55e07528934f6293c4168027d0e53d07facc6630cb08197e53fb193a171135dc8ad9979402a71b6926bcdcdc47b93401910a5fcc1a813b682b09ba7a72d2486d6c799516465c14729b26949b0b7cbc7c640f267fed80b162c51fd8e09227c101d505a8fae8a2d7054e28a78ba8750decf9057c83979f7abb084945648006c5b28804f34e73b238111a65a1f500b1cc606a848f2859070beba7573179f36149cf5801bf89a1c38cc278415528d03bdb943f96280c8cc52042d9b91faa9d6ea7bcbb7ab1897a3266966f78393426c76d8a49578b98b159ebb46ee0a883a270d8057cd0231c86906a91dbbade6b2469581e2bca2fea8389f7c74bcd70961ea5b934fbcf9a6590bf86b8db548854d9a3fb30110433bd7a1b659ca8568085639237b3bdc37b7fa716d482a25b54106b3a8f54d3aa99b5123da96066904592f3a54ee23a7981ab608a2f4413cc658946c6d7780ea765644b3cc06c70034ab4eb351912e7715b56755d09021571bf340ab92598a24e811893195b96a1629f8041f58658431561fc0ab15292b913ec473f04479bc145cd4c563a286235646cd305a9be1014e2c7b130c33eb77cc4a0d9786bd6bc2a954bf3005778f8917ce13789bbb962807858b67731572b6d3c9b4b5206fac9a7c8961698d88324a915186899b29923f08442a3d386bd416bcc9a100164c930ec35eafb6ab35851b6c8ce6377366a175f3d75298c518d44898933f53dee617145093379c4659f68583b2b28122666bec57838991ff16c368dd22c36e780c91a3582e25e19794c6bf2ab42458a8dd7705de2c2aa20c054e84b3ef35032798626c248263253a71a11943571340a978cd0a602e47dee540a8814ba06f31414797cdf6049582361bbaba387a83d89913fe4c0c112b95621a4bda8123a14d1a842fb57b83a4fbaf33a8e552238a596aae7a150d75da648bc44644977ba1f87a4c68a8c4bd245b7d00721f7d64e822b085b901312ec37a8169802160cce1160f010be8cbcace8e7b005d7839234a707868309d03784b4273b1c8a160133ed298184704625f29cfa086d13263ee5899123c596ba788e5c54a8e9ba829b8a9d904bc4bc0bbea76bc53ff811214598472c9c202b73eff035dc09703af7bf1babaac73193cb46117a7c9492a43fc95789a924c5912787b2e2090ebbcfd3796221f06debf9cf70e056b8b9161d6347f47335f3e1776da4bb87c15cc826146ff0249a413b45aa93a805196ea453114b524e310aedaa46e3b99642368782566d049a726d6cca910993aed621d0149ea588a9abd909dbb69aa22829d9b83ada2209a6c2659f2169d668b9314842c6e22a74958b4c25bbdcd293d99cb609d866749a485dfb56024883cf5465dba0363206587f45597f89002fb8607232138e03b2a894525f265370054b48863614472b95d0a2303442e378b0dd1c75acbab971a9a8d1281c79613acec6933c377b3c578c2a61a1ec181b101297a37cc5197b2942f6a0e4704c0ec63540481b9f159dc255b59bb55df496ae54217b7689bd51dba0383a3d72d852ffca76df05b66eeccbd47bc53040817628c71e361d6af889084916b408a466c96e7086c4a60a10fcf7537bb94afbcc7d437590919c28650c4f2368259226a9bfda3a3a0ba1b5087d9d76442fd786c6f81c68c0360d7194d7072c4533aea86c2d1f8c0a27696066f6cfd11003f797270b32389713cffa093d991b63844c385e72277f166f5a3934d6bb89a4788de28321defc7457ab484bd30986dc1dab3008cd7b22f69702fabb9a1045407da4791c3590ff599d81d688cfa7cc12a68c50f51a1009411b44850f9015dc84a93b17c7a207552c661ea9838e31b95ead546248e56be7a5130505268771199880a141771a9e47acfed590cb3aa7cb7c5f74911d8912c29d6233f4d53bc64139e2f55be75507dd77868e384aec581f3f411db1a742972d3ebfd3315c84a5ad63a0e75c8bca3e3041e05d9067aff3b1244f763e7983d48ba34134bab88d635d8cf8ff5d686058fa68b6c2feeaa5fa4de65757086c0125e937bcc0d02faa8988ae7169df07f6a771e6e7fe3ab65e965c63c3e40ed909'), + c=unhex('e2d5fd4c13cea0b52d874fea9012f3a51743a1093710bbf23950f9147a472ee5533928a2f46d592f35da8b4f758c893b0d7b98948be447b17cb2ae58af8a489ddd9232b99b1c0d2de77caa472bc3bbd4a7c60dbfdca92ebf3a1ce1c22dad13e887004e2924fd22656f5e508791de06d85e1a1426808ed9a89f6e2fd3c245d4758b22b02cade33b60fc889a33fc4447edebbfd4530de86596a33789d5dba6e6ec9f89879af4be4909a69017c9bb7a5e31815ea5f132eec4984faa7ccf594dd00d4d8487e45621af8f6e330551439c93ec078a7a3cc1594af91f8417375fd6088ceb5e85c67099091bac11498a0d711455f5e0d95cd7bbe5cdd8fecb319e6853c23c9be2c763df578666c40a40a87486e46ba8716146192904510a6dc59da8025825283d684db91410b4f12c6d8fbd0add75d3098918cb04ac7bc4db0d6bcdf1194dd86292e05b7b8630625b589cc509d215bbd06a2e7c66f424cdf8c40ac6c1e5ae6c964b7d9e92f95fc5c8852281628b81b9afabc7f03be3f62e8047bb88d01c68687b8dd4fe63820062b6788a53729053826ed3b7c7ef8241e19c85117b3c5341881d4f299e50374c8eefd5560bd18319a7963a3d02f0fbe84bc484b5a4018b97d274191c95f702bab9b0d105faf9fdcff97e437236567599faf73b075d406104d403cdf81224da590bec2897e30109e1f2e5ae4610c809a73f638c84210b3447a7c8b6dddb5ae200bf20e2fe4d4ba6c6b12767fb8760f66c5118e7a9935b41c9a471a1d3237688c1e618cc3be936aa3f5e44e086820b810e063211fc21c4044b3ac4d00df1bcc7b24dc07ba48b23b0fc12a3ed3d0a5cf7671415ab9cf21286fe63fb41418570555d4739b88104a8593f293025a4e3ee7c67e4b48e40f6ba8c09860c3fbbe55d45b45fc9ab629b17c276c9c9e2af3a043beafc18fd4f25ee7f83bddcd2d93914b7ed4f7c9af127f3f15c277be16551fef3ae03d7b9143f0c9c019ab97eea076366131f518363711b34e96d3f8a513f3e20b1d452c4b7ae3b975ea94d880dac6693399750d02220403f0d3e3fc1172a4de9dc280eaf0fee2883a6660bf5a3d246ff41d21b36ea521cf7aa689f800d0f86f4fa1057d8a13f9da8fffd0dc1fad3c04bb1cccb7c834db051a7ac2e4c60301996c93071ea416b421759935659cf62ca5f13ae07c3b195c148159d8beb03d440b00f5305765f20c0c46eee59c6d16206402db1c715e888bde59c781f35a7cc7c1c5ecb2155ae3e959c0964cc1ef8d7c69d1458a9a42f95f4c6b5b996345712aa290fbbf7dfd4a6e86463022a3f4725f6511bf7ea5e95c707cd3573609aadeaf540152c495f37fe6ec8bb9fa2aa61d15735934f4737928fde90ba995722465d4a64505a5201f07aa58cfd8ae226e02070b2dbf512b975319a7e8753b4fdae0eb4922869cc8e25c4a5560c2a0685de3ac392a8925ba882004894742e43ccfc277439ec8050a9aeb42932e01c840dfcedcc34d3991289a62c17d1284c839514b93351dbb2dda81f924565d70e7079d5b8126caab7a4a1c731655a53bcc09f5d63ec9086dea650055985edfa8297d9c95410c5d1894d17d5930549adbc2b8733c99fe62e17c4de34a5d89b12d18e42a422d2ce779c2c28eb2d98003d5cd323fcbecf02b5066e0e734810f09ed89013c00f011bd220f2e5d6a362df90599198a093b03c8d8efbfe0b617592faf1e64220c4440b53ffb47164f369c95290ba9f3108d686c57db645c53c012e57af25bd6693e2cc6b57651af1591fe5d8916640ec017c253df0606bb6b3035fae748f3d4034223b1b5efbf5283e778c1094291cf7b19be0f317350e6f8518fde0efb1381fb6e16c241f7f17a5210693a274159e7fac868cd0dc4359c3d9eefea0d9e31e43fa651392c65a543a59b3eee3a639dc9417d056a5ff0f160beee2eac29a7d88c0982cf70b5a46379f21e506aac61a9bb1b8c2b9dab0e44a823b61d0aa11d94f76a4a8e21f9d4280683208f4ea911116f6fd6a97426934ec3426b8c8f703da85e9dcf99336136003728b8ecdd04a389f6a817a78bfa61ba46020bf3c34829508f9d06d1553cd987aac380d86f168843ba3904de5f7058a41b4cd388bc9ce3aba7ee7139b7fc9e5b8cfaaa38990bd4a5db32e2613e7ec4f5f8b1292a38c6f4ff5a40490d76b126652fcf86e245235d636c65cd102b01e22781a72918c'), + k_expected=unhex('7264bde5c6cec14849693e2c3c86e48f80958a4f6186fc69333a4148e6e497f3')) + # Decaps test from test group 4, test case 77 (reject) + decaps_test('mlkem512', + dk=unhex('69f9cbfd1237ba161cf6e6c18f488fc6e39ab4a5c9e6c22ea4e3ad8f267a9c442010d32e61f83e6bfa5c58706145376dbb849528f68007c822b33a95b84904dcd2708d0340c8b808bcd3aad0e48b85849583a1b4e5945dd9514a7f6461e057b7ecf61957e97cf62815f9c32294b326e1a1c4e360b9498ba80f8ca91532b171d0aefc4849fa53bc617932e208a677c6044a6600b8d8b83f26a747b18cfb78beafc551ad52b7ca6cb88f3b5d9ce2af6c67956c478cef491f59e0191b3bbe929b94b666c176138b00f49724341ee2e164b94c053c185a51f93e00f36861613a7fd72febd23a8b96a260234239c9628f995dc13807b43a69468167cb1a8f9dd07ee3b33238f63096ebc49d5051c4b65963d74a4766c226f0b94f1862c2124c8c749748c0bc4dc14cb34906b81c5524fb8100798542dc6cc2aa0a708575eabcc11f96a9e61c017a96a7ce93c42091737113ae783c0ae8755e594111edfabfd86c3212c612a7b62afd3c7a5c78b2f07344b789c2b2dbb5f4448be97bba4233c0039c0fe84300f9b03ac99497e6d46b6e95308ff84790f612cf186ec16811e80c179316a63b25703f60b842b61907e62894e736647b3c09da6fec5932782b36e0635085a3949e694d7e17cba3d9064330438c071b5836a770c55f6213cc1425845de5a334d75d3e5058c7809fda4bcd78191da9797325e6236c2650fc604ee43a83ceb34980084403a33259857907799a9d2a713a633b5c904727f61e42520991d655705cb6bc1b74af60713ef8712f14086869be8eb297d228b325a0609fd615eab7081540a61a82abf43b7df98a595be11f416b41e1eb75bb57977c25c64e97437d88ca5fda6159d668f6bab8157555b5d54c0f47cbcd16843b1a0a0f0210ee310313967f3d516499018fdf3114772470a1889cc06cb6b6690ac31abcfaf4bc707684545b000b580ccbfcbce9fa70aaea0bbd9110992a7c6c06cb368527fd229090757e6fe75705fa592a7608f050c6f88703cc28cb000c1d7e77b897b72c62bcc7aea21a57729483d2211832bed612430c983103c69e8c072c0ea7898f2283bec48c5ac81984d4a5a83619735a842bd172c0d1b39f43588af170458ba9ee7492eaaa94ea53a4d38498ecbb98a5f407e7c97b4e166e397192c216033014b878e938075c6c1f10a0065abc3163722f1a2effec8d6e3a0c4f7174fc16b79fb5186a75168f81a56aa48a20a04bddf182c6e179c3f69061555ef7396dd0b7499601a6eb3a96a9a22d04f1168db56355b07600a20370637b645976bbd97b6d6288a0d3036360472e3ac71d566db8fbb1b1d76cb755cd0d68bdbfc048eba2525eea9dd5b144fb3b60fbc34239320cbc069b35ab16b8756536fb33e8a6af1dd42c79f48ad120ae4b159d3d8c319060cce569c3f6035365585d34413795a6a18ec5136ab13c90e3af14c0b8a464c86b9073222b56b3f7328aea798155325911250ef016d72802e3878aa50540cc983956971d6efa352c02554dc760a5a91358ea56370884fd5b3f85b70e83e4697deb1705169e9c60a74528cf15281cb1b1c457d467b5f93a60373d10e0cf6a837aa3c9596a72bec29b2d7e58653d533061d381d51759752217eb46cac7807c4ad38b611644acf0a3f26b6b084ab47a83bf0d696f8a4768fc35bca6bc7903b2a237c27749f5510c863869e6ae56bb2afe4771c9221874f50f5b14baad5993b49238fd0a0c9f79b7b4584e41301f7a885c9f91819bea00d512581730539fb37e59e86a6d19ca25f0a811c9b428ba8614aa4f94807bc031cbcc183f3bf07fe2c1a6eba80d5a706ee0dab27e231458025d84a7a9b0230501116c290a6bb50626d97b939850942828390b0a2001b7853ad1ae9b011b2db36caeea73a2328e3c56485b491c299115a017c907ab54317260a593a0d7ba6d06615d6e2ca84b860eff3ccb597211bfe36bdef8069afa36c5a73392722650e4957dca597acba5605b63c163cfa94b64ddd62301a4332083361972589db0599a694dd4547a5ee9196577c22ed427ac89bb8ba3753eb76c41f2c1129c8a77d6805fa719b1b6ca11b740a78a3d41b5330526ab87d58d5925315a1485edc647c1604eb38138de637ad2c6ca5be44e1008b2c0867b229ccc36619e2758c4c2029eaeb26e7a803fca305a59cd585e117d698ece011cc3fce54d2e114545a21ac5be6771ab8f13122fad295e745a503b142f91aef7bde99998845fda043555c9c1ee535be125e5dce5d266667e723e67b6ba891c16cba174098a3f351778b0888c9590a9090cd404'), + c=unhex('5c26d456c6c7b0e8df0b125e5d5428fe393655127a5e05bdd1bcac14c47493783097b6185058fa700555dd8af10f0f979a39a603826ffeb0b44e9487539f3f1a07c673e96640ddf754c8b98cd83473568b49d095f682c1acf0e160ab93eb41a16a57d53b419620d351c837315080d530845cf8d63cfccdb6e9dfbe220a2c14221aa392e6337fa364df0d2e0398f15ac3dc822b5dd7217081107a45c8cb8eaca51e034117962aee7ec0ee212fa67a5d4b07d355a0981e4285116ecf5ca9fab6e3105e4de4aec5e32938a1eb91e65ce7b39c3b9829aa1e72b8092c3622e519ee092fac8106d6597ceb941c763288723cb55044a36d4181052a78b424b0de1b0260f624a8d3b317095371ee9beea9272250d598ac63c2138d23f99087777a902eba2163171a07546b72fce7f86ee3b1dc1b8eac85440b8d241742c3771f91bf981909e4f3e2505c594761259ed3aada6aa09181b99037a395d66e6ee4bbef97de6ba36c53a1808cba50938038c151603105bd6a4199ea44bf4b08961672598cb708f896e03cd9b8f8ad89decfbe6be0ef0006b7bd2f4aa6eb21c0218ede601d46924cf391ae3a44e43d96ebe84a630937c3409ef0710970c27e3add4e64dc64e83942abea9ccf498ef1fe72b254043d2775a37e0b5ddd3f596ea131e0734afa9d0223f4cd9d1ab7304ca979ad37f717bedc3a9526f8fc94433fe4614f82e709456f39bee7bacc84e5a70114af1c2ac8b9b3faa81c8f35f5a5d24189e1a457f58166473f5f1df0170aab5e4ac8fc719f945ccbe6f2fed24b23321d95c4c850b278b8c4ea02e3098d5a599aa3d842cf889b7f284ac5e6e66386d63f2c860b997966b4df2c32288a50045012b7362727b856af4f8258509b563758752ffbb1040f3c2ad8b0ded64fc15c95c1a16de0dae6625a9effce190fc7f3261d844c114913c6b1152a258a37761b81879b59c37a1dfac07c3e934510b45da44c2581a79dafbf00fabb207306269d9b74b93f4367b3ba22ccc51b362de16e49d9fdbf8cff84f6ce6892ca2245d34ceb9c8759e702832b66a572de9f3016a38f7328700f96b2e947'), + k_expected=unhex('a4a24e182fea12ff128ab2d4afe6569817513ffc547db70636752c9c66c002b8')) + # Decaps test from test group 5, test case 86 (reject) + decaps_test('mlkem768', + dk=unhex('1e4ac87b1a692a529fdbbab93374c57d110b10f2b1ddebac0d196b7ba631b8e9293028a8f379888c422dc8d32bbf226010c2c1ec73189080456b0564b258b0f23131bc79c8e8c11cef3938b243c5ce9c0edd37c8f9d29877dbbb615b9b5ac3c948487e467196a9143efbc7cedb64b45d4acda2666cbc2804f2c8662e128f6a9969ec15bc0b9351f6f96346aa7abc743a14fa030e37a2e7597bddfc5a22f9cedaf8614832527210b26f024c7f6c0dcf551e97a4858764c321d1834ad51d75bb246d277237b7bd41dc4362d063f4298292272d01011780b79856b296c4e946658b79603197c9b2a99ec66acb06ce2f69b5a5a61e9bd06ad443ceb0c74ed65345a903b614e81368aac2b3d2a79ca8ccaa1c3b88fb82a36632860b3f7950833fd0212ec96ede4ab6f5a0bda3ec6060a658f9457f6cc87c6b620c1a1451987486e496612a101d0e9c20577c571edb5282608bf4e1ac926c0db1c82a504a799d89885ca6252bd5b1c183af701392a407c05b848c2a3016c40613f02a449b3c7926da067a533116506840097510460bbfd36073dcb0bfa009b36a9123eaa68f835f74a01b00d2097835964df521ce9210789c30b7f06e5844b444c53322396e4799baf6a88af7315860d0192d48c2c0da6b5ba64325543acdf5900e8bc477ab05820072d463affed097e062bd78c99d12b385131a241b708865b4190af69ea0a64db71448a60829369c7555198e438c9abc310bc70101913bb12faa5beef975841617c847cd6b336f877987753822020b92c4cc97055c9b1e0b128bf11f505005b6ab0e627795a20609efa991e598b80f37b1c6a1c3a1e9aee7028f77570ab2139128a00108c50eb305cdb8f9a603a6b078413f6f9b14c6d82b5199ce59d887902a281a027b717495fe12672a127bbf9b256c43720d7c160b281c12757da135b1933352be4ab67e40248afc318e2370c3b8208e695bdf337459b9acbfe5b487f76e9b4b4001d6cf90ca8c699a174d42972dc733f33389fdf59a1daba81d834955027334185ad02c76cf294846ca9294ba0ed66741ddec791cab34196ac5657c5a78321b56c33306b5102397a5c09c3508f76b48282459f81d0c72a43f737bc2f12f45422628b67db51ac1424276a6c08c3f7615665bbb8e928148a270f991bcf365a90f87c30687b68809c91f231813b866bea82e30374d80aa0c02973437498a53b14bf6b6ca1ed76ab8a20d54a083f4a26b7c038d81967640c20bf4431e71dacce8577b21240e494c31f2d877daf4924fd39d82d6167fbcc1f9c5a259f843e30987ccc4bce7493a2404b5e44387f707425781b743fb555685584e2557cc038b1a9b3f4043121f5472eb2b96e5941fec011ceea50791636c6abc26c1377ee3b5146fc7c85cb335b1e795eec2033ee44b9aa90685245ef7b4436c000e66bc8bcbf1cdb803ac1421b1fdb266d5291c8310373a8a3ce9562ab197953871ab99f382cc5aa9c0f273d1dca55d2712853871e1a83cb3b85450f76d3f3c42bab5505f7212fdb6b8b7f6029972a8f3751e4c94c1108b02d6ac79f8d938f05a1b2c229b14b42b31b01a364017e59578c6b033833774cb9b570f9086b722903b375446b495d8a29bf80751877a80fb724a0210c3e1692f397c2f1ddc2e6ba17af81b92acfabef5f7573cb493d184027b718238c89a3549b8905b28a83362867c082d3019d3ca70700731ceb73e8472c1a3a093361c5fea6a7d40955d07a41b64e50081a361b604cc518447c8e25765ab7d68b243275207af8ca6564a4cb1e94199dba1878c59bec809ab48b2f211badc6a1998d9c7227c1303f469d46a9c7e5303f98aba67569ae8227c16ba1fb3244466a25e7f823671810cc26206feb29c7e2a1a91959eeb03a98252a4f7412674eb9a4b277e1f2595fca64033b41b40330812e9735b7c607501cd8183a22afc3392553744f33c4d202526945c6d78a60e201a16987a6fa59d94464b56506556784824a07058f57320e76c825b9347f2936f4a0e5cdaa18cf8833945ae312a36b5f5a3810aac82381fdae4cb9c6831d8eb8abab850416443d739086b1c326fc2a3975704e396a59680c3b5f360f5480d2b62169cd94ca71b37bc5878ba2985e068ba050b2ce50726d4b4451b77aaa8676eae094982210192197b1e92a27f59868b78867887b9a70c32af84630aa908814379e6519150ba16439b5e2b0603d06aa6674557f5b0983e5cb6a97596069b01bb3128c416680657204fd07640392e16b19f337a99a304844e1aa474e9c799062971f672268960f5a82f950070bbe9c2a71950a3785bdf0b8440255ed63928d257845168b1eccc4191325aa76645719b28ebd89302dc6723c786df5217b243099ca78238e57e64692f206b177abc259660395cd7860fb35a16f6b2fe6548c85ab66330c517fa74cdf3cb49d26b1181901af775a1e180813b6a24c456829b5c38104ece43c76a437a6a33b6fc6c5e65c8a89466c1425485b29b9e1854368afca353e143d0a90a6c6c9e7fdb62a606856b5614f12b64b796020c3534c3605cfdc73b86714f411850228a28b8f4b49e663416c84f7e381f6af1071343bf9d39b45439240cc03897295fea080b14bb2d8119a880e164495c61bebc7139c11857c85e1750338d6343913706a507c9566464cd2837cf914d1a3c35e89b235c6ab7ed078bed234757c02ef6993d4a273cb8150528da4d76708177e9425546c83e147039766603b30da6268f4598a53194240a2832a3d67533b5056f9aaac61b4b17b9a2693aa0d58891e6cc56cdd772410900c405af20b903797c64876915c37b8487a1449ce924cd345c29a36e08238f7a157cc7e516ab5ba73c8063f726bb5a0a0319e57127438c7fc601c99ccaae4c1a83726fdcb5045ed1a82a985ea995396d77272c66ce493289f6110910f37c2741ce47026a6f8261999c6482572b1693912ef12eebea7acf9234fb409f2a6090e6b0bfd895469d0b2a921bb723f87a33ea5465ab90f514b67698c0768b6ca498b022c512fa0875f054aa2265867e31c0e522651e024a07d60dd9f633166921f4126bc2b6aa01cc15a09b85bff8218c5aae95bc1ffb26ae5a137670f04910ca9d7241b6660c394c5455917746a26682fb71a432ea9530e839bdeb07433004f45a0ddaa0b24e3a566a540815f281e3fc259ac6cbc0acb8d62268b603bc676ab415c474bb94873e4487ae31a4e3845c79901550890ee8784eef904fee62ba8c5f952c68413052e0a7e3388bb8ff0ad602ae3ea14d9df6dd5e4cc6a381a41da5c137ecc49df587e178eaf47702ec623780691a3233f69f12bd9c9b9637c51378ad71a831055277254cc63c5ad4cb76b4ab82e5fca135e8d26a6b3a89fa5b6f'), + c=unhex('74a26c7d27146a22c7eab420134e973799cec1da2df61ae0fa7905a3a47485a063076bfa22d6e4fe5059de0a32e38f11abd63f990e91bd0e3a5bc6e710dfe5dc0f6d4a18147ebc2e2d9b179374d83692c53efbd45f28a2a928c2494f903576c410eb1773895ebeadb119960eebda9c3c710795a6d9b781fc58b30d08107f4e20944a382afb079f31d21724f2c26e6a53412f0a908be7586f2b3d6d7c1dea0270e98aa209244bd88ed68aae01432342ba5f49e015cb476b5b78d15ea77a354cc9e9fd07137d8760be42fd4746c62c02028e7b405ddc95df3d021921cfeddb3d961b957eca302a263dab2dc117beb3e79efacfcf936dfc09fc0d19c358d724fa381ea06ca067c384e944302c3907ab15a1da4b41352692add59b061541f07eff25ec42f46e1a0e370cad06ff3fd997d4d2c5648af762231b382d0593401936cba21551a2ae30d8e8effcf43916b83138bb5e610364429879fa9cdd5b7d3cf2feabaa1dc8d50ce69402e21103e795df7074d1fcf65f8a4e18986d5417780602c63be5a044863384bd3d8ffb685eac567ed8349dcf2ceb702b7375b145729998049d13e2cd466cf2231b9d3a20018ee908f8514a6c6a89df7232f91fcd84b81ebc8bc539e9a37a4324755564be1bf4fa1fb4571e0abbc9b52f9d090c33be599de6c8532c7cb7ec8b4e2d3c07505280e99923865903ffd18bc13b9c8164aa1eae84e38d3f57fdb8801785f105a6a8574bd2fe9bf305848e525330bc2d24f0257e47a4950f433a9233e8cdeba81dbae7d8c1a06d01f70de6ef663207d84952827bab3d451cbea0990007fbdb4240fe899a706f7c1563e05c70be9d575189ef83e0cf76195f6652491cce04f1ce2092170a92e0dd7301246a4c44fc0b4ee6aaa63fc7027840abd2ec25f654589738cd38b9e10b975cfb6c1d2eb4da97736998f84fdddd810d72da3c5ab13507420ddbfaa4f7750c1fae9c7dfb30f40a12aea689fc78da900020e3abb32a364d5c6b3c7544a1b5734a41e95c8314b448cd0b738d829af772a8f81c51adba2d85f326c8f5d6961cf12d44a9bedea00d1df5b48f429b1ce0c15ea5f5bc10b017247ba2c6be922b0563b8e9698677cb6c45ccf2081bf84219d2904c11ff92199f8aefad62d8608e200802c5a07202cc820e9e520e31bf36a83002eca4018b0b3a398801562aa86c77ab0d50a8fbc3768b0a643b97e7f9072168de29b8175999c9aa48d301a3f0303172e9c7d4f16329d5ca9d42397c3982e10c9da42de88bd6c2ab91c1e71e778e58bb8f801f207a88a9b47f9c687afbba34eda6d2899e4fa0008aa2b539711753dc7c07f614e814f683d6c037562ae1fbbe6d7d5fa54b7a6d9451e11b01aaccc3bf2ed64742dd100e0eab2df6cccf937b6d5981eca0e01f3245cf26a72ad1adf066c8f5430d72f509963a657d85e554c14e26e8bec5d5f3ab998c9b29f16b04747d80749b30e51fd2a7f690c22f9986aaf6358d6fab8ded54971b32641de2b258590eeaa6bf1f32324a7c4c983f49466d86'), + k_expected=unhex('3d23b10df232a180786f61261e85278251746580bebca6acbad60aef6952be69')) + # Decaps test from test group 6, test case 97 (reject) + decaps_test('mlkem1024', + dk=unhex('8445c336f3518b298163dcbb6357597983ca2e873dcb49610cf52f14dbcb947c1f3ee9266967276b0c576cf7c30ee6b93dea5118676cbee1b1d4794206fb369aba41167b4393855c84eba8f32373c05bae7631c802744aadb6c2de41250c494315230b52826c34587cb21b183b49b2a5ac04921ac6bfac1b24a4b37a93a4b168cce7591be6111f476260f2762959f5c1640118c2423772e2ad03dc7168a38c6dd39f5f7254264280c8bc10b914168070472fa880acb8601a8a0837f25fe194687cd68b7de2340f036dad891d38d1b0ce9c2633355cf57b50b896036fca260d2669f85bac79714fdafb41ef80b8c30264c31386ae60b05faa542a26b41eb85f67068f088034ff67aa2e815aab8bca6bf71f70ecc3cbcbc45ef701fcd542bd21c7b09568f369c669f396473844fba14957f51974d852b978014603a210c019036287008994f21255b25099ad82aa132438963b2c0a47cdf5f32ba46b76c7a6559f18bfd555b762e487b6ac992fe20e283ca0b3f6164496955995c3b28a57bbc29826f06fb38b253470af631bc46c3a8f9ce824321985dd01c05f69b824f916633b40654c75aaeb9385576ffde2990a6b0a3be829d6d84e34f1780589c79204c63c798f55d23187e461d48c21e5c047e535b19f458bba1345b9e41e0cb4a9c2d8c40b490a3babc553b3026b1672d28cbc8b498a3a99579a832feae74610f0b6250cc333e9493eb1621ed34aa4ab175f2ca231152509acb6ac86b20f6b39108439e5ec12d465a0fef35003e14277a21812146b2544716d6ab82d1b0726c27a98d589ebdacc4c54ba77b2498f217e14e34e66025a2a143a992520a61c0672cc9cced7c9450c683e90a3e4651db623a6db39ac26125b7fc1986d7b0493b8b72de7707dc20bbdd43713156af7d9430ef45399663c2202739168692dd657545b056d9c92385a7f414b34b90c7960d57b35ba7dde7b81fca0119d741b12780926018fe4c8030bf038e18b4fa33743d0d3c846417e9d5915c246315938b1e233614501d026959551258b233230d428b181b132f1d0b026067ba816999bc0cd6b547e548b63c9eaa091bac493dc598dbc2b0e146a2591c2a8c009dd5170aae027c541a1b5e66e45c65612984c46770493ec896ef25aa9305e9f06692cd0b2f06962e205bebe113a34ebb1a4830a9b3749641bb935007b23b24bfe576956254d7a35aa496ac446c67a7fec85a60057e8580617bcb3fad15c76440fed54cc789394fea24452cc6b0585b7eb0a88bba9500d9800e6241afeb523b55a96a535151d1049573206e59c7feb070966823634f77d5f1291755a243119621af8084ab7ac1e22a0568c6201417cbe3655d8a08dd5b513884c98d5a493fd49382ea41860f133ccd601e885966426a2b1f23d42d82e24582d99725192c21777467b1457b1dd429a0c41a5c3d704cea06278c59941b438c62727097809b4530dbe837ea396b6d31077fad3733053989a8442aac4255cb163b8ca2f27501ea967305695abd659aa02c83ee60bb574203e9937ae1c621c8ecb5cc1d21d556960b5b9161ea96fffebac72e1b8a6154fc4d88b56c04741f090cbb156a737c9e6a22ba8ac704bc304f8e17e5ea845fde59fbf788cce0b97c8761f89a242f3052583c6844a632031c964a6c4a85a128a28619ba1bb3d1bea4b49841fc847614a066841f52ed0eb8ae0b8b096e92b8195405815b231266f36b18c1a53333dab95d2a9a374b5478a4a41fb8759957c9ab22cae545ab544ba8dd05b83f3a613a2437adb073a9635cb4bbc965fb454cf27b298a40cd0da3b8f9ca99d8cb4286c5eb476416796070ba535aaa58cdb451cd6db5cbb0ca20f0c71de97c30da97ec7906d06b4b939396028c46ba0e7a865bc8308a3810f1212006339f7bc169b1666fdf475911bbc8aaab41755c9a8aabfa23c0e37f84fe46999e030494b9298ef9934e8a649c0a5cce2b22f31809afed23955d87881d99fc1d352896cac9055bea0d016ccba7805a3a50e221630379bd01135221cad5d9517c8cc42637b9fc0718e9a9bb4945c72d8d11d3d659d83a3c419509af5b470dd89b7f3accf5f35cfc322115fd66a5cd2875651326f9b3168913be5b9c87ae0b025ec7a2f4a072750946ac61170a7826d9704c5a23a1c0a2325146c3bc1858826c6b39279c2da7438a370ed8a0aa5169e3bec29ed88478732758d454143e227f8595883297842e6af133b17e4811b0f5713ac73b7e347423eb92822d2306fa14500a7207a0672672046544acc4ea9c16ed7421a069e0d737a98628519c6a29a424a868b46d9a0cc7c6c9ddd8b8bcbf422c8f48a73143d5abb66bc55499418430802bac544463cc7319d17998f29411365766d04c847f3129d9077b7d8339bfb96a6739c3f6b74a8f05f9138ab2fe37acb57634d1820b50176f5a0b6bc2940f1d5938f1936b5f95828b92eb72973c1590aeb7a552ceca10b00c303b7c75d402071a79e2c810af7c745e3336712492a42043f2903a37c6434cee20b1d159b057699ff9c1d3bd68029839a08f43e6c1c819913532f911dd370c7021488e11cb504cb9c70570fff35b4b4601191dc1ad9e6adc5fa9618798d7cc860c87a939e4ccf8533632268cf1a51aff0cb811c5545cb1656e65269477430699ccdea3800630b78cd5810334ccf02e013f3b80244e70acdb060bbe7a553b063456b2ea807473413165ce57dd563473cfbc90618ade1f0b888aa48e722bb2751858fe19687442a48e7ca0d2a29cd51bfd8f78c17b9660bfb54a470b2ae9a955c6ab8d6e5cc92ac8ed3c185daa8bc29f0578ebb812b97c9e5a848a6384de4e75a31470b53066a8d027ba44b21749c0492465f9072b28376c4e290b30c1863f9e5b79996083422bd8c272c10ecc6eb9a0a8225b31aa0a66e35b9c0b9a79582ba20a3c04cd29914f083a0158288ba4d6eb62d87264b912bca39732fbde536a377ad02b8c835d4a2f4e7b1ce115d0c860beaa7955a49ad689586a89a2b9f9b10d1595d2fc065ad018a7d56c614471f8e946fe8ab49e8226591119fcadb4f9a861631378736b6688b782d58e97e4572753a9664b6b8536812b25911aa76a242375433192738eee762f6b84315bb3436231e0a9b277ed28ae0050728346457e13405062db2804b8da60bb5c793d4cc0e101cba2d9182fd7124ff52bf4ca28292ac26d678088953971dba0b6fec2c9659353291c70c5b9245a0ca253304afd3c95102bea66875c6201680b4bda38687b648c28eb37478e3bc00ca8a3cc27204642b42b68fcbe7b21a366d0668a5029a7deef94cdd6a95d7ea8931673bf7112d4042107b1b8b9700c974f9c4e83a8facd89bfe0ca3cc4c2fce80a03d3576c222a792b72b1f070ab7f6b6f2b5ca2af5054afa70a896990159b45d1003e2a05648675e596016f1b71dd0f7bda7e2097fc73b3a143d12c726020ac34958ad7062b92b9abf3ca6be5ae29f57135e625a367971837e6363d1532094e022a23467cf932e1f89b5b0803c1ec99b585a78b5865096746f32258214ecb38065c97f455e155acc2dd005a9c76bed59cda73837d303504e6c976a606a2be7bbec5948b91a349e8936688cc0279754b743abc58666b19b6c3260051f19206bb962bb6633eb0048e32baacc5b020d02c86ca9770ad469db54a106ac73a35b8057422b3db202c5a5b4e3d535f0fc99326c4b8b7b16f1cb5af96803fa8c195fc0bceddaaf012a51728b76489082373c91e92c87acca795160782e3b0dd643544bb96abc2708d49b759cf057aa223bafd96a330baf39810fe8671b4343c297da1e1969c996216ab5106da668941b160d4477017136cbca5b5a8d44c4a8b1cf3ef79785e5aa25c3a1ad6c24fd140f79207de5a499f8a1534ffa804aa7b3889cbe25c0414704aa57897f17862364eca56258007248813912b836497f0359c2f7238a05d305a0ea152e72b44417a868134e91b3ca7931232fd4c25f8c2a492a339cdc0a138967211451f2562678fa14080a34436c42b07865ac036a81e97a7787a938025caf813450368bed0c94b1857604526405d27a1c1abc81b5b6ec13c71930a97d9232cf7021ef87a4d155328e62b583a83b4af21f9f5750f8575150424f63b899d71cad267c09e4467146e16e9b6c653f008c311375e2e006d4076a546b82f5314222f7c654317e79ec6035b73faf491757e61c828326d53044541c4d4537abd3ea1e67998c3382974ca78ae1b1960e4a9226b0219ab070f0d7aa66d76f9316adb80c54d6499771b471e8168d47bcaa08324ab6ba92c3a70275f24fa4dc10e251633fb98d162bb5537202c6a553ce7841c4d40b873b85ca03a0a1e1cfade6ba5180ab1323ccba9a3e9c53d37575ab1fd9e7316c6feecb0a14df6f2da56c2f56f55a89635cfcfda47927af1f0a47b2d4e4e61634b1b51d37a3a307a972420de1b7a481b83e583b6af16f63cb00c6'), + c=unhex('4f90106ff7c3dc4e47417f31ab56b1c5e426c1ecd5878aad2b705e75062da5fa6f4d18b704c941c6c6d941fd21191a69210bc39e24950d9f851b6de8ce30023dc7536439104d42245f3e04e6aa6763f8ac97adbd04cc69547bce0bf290ffb5d12946301174af1b0868c14d4293fa9dcc5b23f809b02cc78defe7f27935b9b681e531fc21ccb2af8ef6144d8498e63e0ee48af8d4cef7ac1f669ac740b06f79ddb58e794f2fc2ca832e05a0374c18a4f2cc78343eea064abc5f468f4dd11e0b6e8fa1d18a221d8241450c05eb9edf90d9d7f666ac82e7fd44af9328e0bc6004d5b114e80e9b980d18e081d771dfcb2acfd40142a2eb33234f75733eab7d8ee8a5a6f796681a4a8af85cce86971b821d4ad8371049e94e280b77b15d111a42aeadfc08d4f804bd78885443e81a393df7c8754c460915846e09a0596587460038f55d06ec21434a1c2df44d0c16706e8d2b83f0e7833976ef05bf1d9f0ddc9a37597e401b817c2bec8e02eb9df7591e239f25f8648e7f2f4f673093bd9cb703da32b353f58514c6ab55748b194e52f153d52f5f33fe95c5f9f65ea97ba721e8ddf333b64d233a867a12701e00c5d8a9b5ae344f3d847c27c079dcc9c3b40ec4604a9f041e7987e8b930c658b9a132de4e422c0e27553a2a0eab8c859eb0e5677e83272725c5c1652e61b9bbf5c9c59bc2357a4d1db9c607f34dc1ba074b84dfc69e4097a7ad2ba9a58000027296ad39fc1ce218a5eec7adfa8aa3b9100b0b603cfc83c152589e12e6bd9ee10c49131a701d315dfec38e018328916f9ffaa7305cfb66781707d2d1020eb782f9f003db4e46b87d693f62e8bde170141ff71f26ddf5310c00c9163655f5217dd2c8b0466ac89db55bd7fb3b0964bc9009e9686185117dcb50d6d0297753cf7f1217e819ee60e3f0faec4a5af0c2ea83ccde15cf045c6961de8ff6235c9d93ba4c89b7a82a7471fcfb0b8ead54d56e8a1de21b3933ac5b4a0689eef3598926e17bbb16aec61ec30a2ccc0e0323ec282887c108c3a4e83e3666493d8653d0e92443808c79d770bff48a49e65ae089fec790bba4c66354ef67a334c1ea5c6c5707b6928ebd1bdb6a940fa242c6ebd7f3e71272421c9082841a6cad2894bb8ac85f105d8bbc9e6f0a3df0d7c46f6e2f4cab904ed157afa85d4a852220a9636e1e8821643a9e4028d87a430432f09354b3973182385cf5abfc8f84982bee0bcbf5d18637399163a09eb45711e07c4458498c76979107cf91b3fc590ea4ad715d656d5e56dc32146580101c952e02ed7017960d54caaccc70607196980adbdaea420a52c0559ed23c9514f8ca7ab7f3baafd2fab58960a64128d5a50e9ad8db7d23a90ce64c1bc349d118d3603358377f84ff5a64457fa1cf41b27094bca72360bd429415b9ef9accb7a5d7b9e5f5fdca8fcfa4592e91d7e5120df7e3c6675af2211bb94d856a5d2285fbbb36984a1345590930b13232565d54812a9345324c232653190323cc67c840e478d09e6ddbcf999f7aa3b556f80332e67aca41ec0661088d7696bb64e9a98a0749faa9854d9b48754023bacaf3c8081a46157c6453bdc89341d3092f3b5337874ce5de559a56a2ffb7f401f6e28eecaf4fde5b60dea73d6b2182ef68e07a8297f3c959e17139b5dedc72c7a0e103aff866e89d1f62a1f6b97b61bc059bde5a2a06087ef783a441f23dd191c692d03c097ff9ee831f7715c6e508bf475e79a8353e84b06a9356045c8fd09fba35879069b9a3f478fbd051143c13d753bc45f3040e85985efd6b149efa9455a18e2894e6ea0be58f451ff1156f93cc7117b5d091e9dd50d41bfccd44f2c4eb7812aefd13c8b68d7f0103bb6ca38d233b6aadd01845b7e44d13c1cb1577d6c4354b063991344787f8c0be667a7440b98917ad64cc2ef2bc82efc3398b3b1b238540756ce9fc5edd26cc20e761d592a1a0530aa8befcfe8dadbac99a417ca0827f4983ff5be656669f2b5f985ff6b16c44bbea131d1fcc70fc53bf31ef225d1f5d41863b51b57ea65c6164f7531ae492efa64161b7daba3ef4586f3459be8a962367dc276597b98e91ff594efe8849bad4cf91b9e5f244cf03ca9615be128e96958533544a56e735994b92e4ef0d5fab54b78ec66641c7463f225d261c144f00a0270741d7a511994833635a8a9b670cbfbef239bf83327e247943b205da68db94e3f3'), + k_expected=unhex('7545cc458e0a274a83b13554224f0bd01d57cc4775ad12468d3fee5b08c93a6a')) + if __name__ == "__main__": # Run the tests, suppressing automatic sys.exit and collecting the # unittest.TestProgram instance returned by unittest.main instead. diff --git a/test/testcrypt-enum.h b/test/testcrypt-enum.h index 4e93c786..586be9fa 100644 --- a/test/testcrypt-enum.h +++ b/test/testcrypt-enum.h @@ -160,6 +160,12 @@ BEGIN_ENUM_TYPE(argon2flavour) ENUM_VALUE("Argon2id", Argon2id) END_ENUM_TYPE(argon2flavour) +BEGIN_ENUM_TYPE(mlkem_params) + ENUM_VALUE("mlkem512", &mlkem_params_512) + ENUM_VALUE("mlkem768", &mlkem_params_768) + ENUM_VALUE("mlkem1024", &mlkem_params_1024) +END_ENUM_TYPE(mlkem_params) + BEGIN_ENUM_TYPE(fptype) ENUM_VALUE("md5", SSH_FPTYPE_MD5) ENUM_VALUE("sha256", SSH_FPTYPE_SHA256) diff --git a/test/testcrypt-func.h b/test/testcrypt-func.h index a39f5788..d1ca3f5c 100644 --- a/test/testcrypt-func.h +++ b/test/testcrypt-func.h @@ -405,6 +405,36 @@ FUNC_WRAPPED(int16_list, ntru_encrypt, ARG(int16_list, plaintext), FUNC_WRAPPED(int16_list, ntru_decrypt, ARG(int16_list, ciphertext), ARG(val_ntrukeypair, keypair)) +/* + * ML-KEM and its subroutines. + */ +FUNC(void, mlkem_keygen, + ARG(out_val_string_binarysink, ek), ARG(out_val_string_binarysink, dk), + ARG(mlkem_params, params)) +FUNC_WRAPPED(void, mlkem_keygen_internal, + ARG(out_val_string_binarysink, ek), + ARG(out_val_string_binarysink, dk), + ARG(mlkem_params, params), + ARG(val_string_ptrlen, d), ARG(val_string_ptrlen, z)) +FUNC_WRAPPED(void, mlkem_keygen_rho_sigma, + ARG(out_val_string_binarysink, ek), + ARG(out_val_string_binarysink, dk), + ARG(mlkem_params, params), ARG(val_string_ptrlen, rho), + ARG(val_string_ptrlen, sigma), ARG(val_string_ptrlen, z)) +FUNC(boolean, mlkem_encaps, + ARG(out_val_string_binarysink, ciphertext), + ARG(out_val_string_binarysink, k), + ARG(mlkem_params, params), + ARG(val_string_ptrlen, ek)) +FUNC_WRAPPED(boolean, mlkem_encaps_internal, + ARG(out_val_string_binarysink, ciphertext), + ARG(out_val_string_binarysink, k), + ARG(mlkem_params, params), + ARG(val_string_ptrlen, ek), ARG(val_string_ptrlen, m)) +FUNC(boolean, mlkem_decaps, ARG(out_val_string_binarysink, k), + ARG(mlkem_params, params), ARG(val_string_ptrlen, dk), + ARG(val_string_ptrlen, ciphertext)) + /* * RSA key exchange, and also the BinarySource get function * get_ssh1_rsa_priv_agent, which is a convenient way to make an diff --git a/test/testcrypt.c b/test/testcrypt.c index b1466dbc..328ffc47 100644 --- a/test/testcrypt.c +++ b/test/testcrypt.c @@ -36,6 +36,7 @@ #include "mpint.h" #include "crypto/ecc.h" #include "crypto/ntru.h" +#include "crypto/mlkem.h" #include "proxy/cproxy.h" static NORETURN PRINTF_LIKE(1, 2) void fatal_error(const char *p, ...) @@ -231,6 +232,7 @@ typedef struct mr_result TD_mr_result; typedef Argon2Flavour TD_argon2flavour; typedef FingerprintType TD_fptype; typedef HttpDigestHash TD_httpdigesthash; +typedef const mlkem_params *TD_mlkem_params; #define BEGIN_ENUM_TYPE(name) \ static bool enum_translate_##name(ptrlen valname, TD_##name *out) { \ @@ -444,12 +446,19 @@ static unsigned *get_out_uint(BinarySource *in) return uval; } -static BinarySink *get_out_val_string_binarysink(BinarySource *in) +static strbuf **get_out_val_string(BinarySource *in) { Value *val = value_new(VT_string); - val->vu_string = strbuf_new(); + val->vu_string = NULL; add_finaliser(finaliser_return_value, val); - return BinarySink_UPCAST(val->vu_string); + return &val->vu_string; +} + +static BinarySink *get_out_val_string_binarysink(BinarySource *in) +{ + strbuf *sb = strbuf_new(); + *get_out_val_string(in) = sb; + return BinarySink_UPCAST(sb); } static void return_val_string_asciz_const(strbuf *out, const char *s); @@ -1031,6 +1040,33 @@ int16_list *ntru_decrypt_wrapper(int16_list *ciphertext, NTRUKeyPair *keypair) return out; } +void mlkem_keygen_internal_wrapper( + BinarySink *ek, BinarySink *dk, const mlkem_params *params, + ptrlen d, ptrlen z) +{ + assert(d.len == 32 && "Invalid d length"); + assert(z.len == 32 && "Invalid z length"); + mlkem_keygen_internal(ek, dk, params, d.ptr, z.ptr); +} + +void mlkem_keygen_rho_sigma_wrapper( + BinarySink *ek, BinarySink *dk, const mlkem_params *params, + ptrlen rho, ptrlen sigma, ptrlen z) +{ + assert(rho.len == 32 && "Invalid rho length"); + assert(sigma.len == 32 && "Invalid sigma length"); + assert(z.len == 32 && "Invalid z length"); + mlkem_keygen_rho_sigma(ek, dk, params, rho.ptr, sigma.ptr, z.ptr); +} + +bool mlkem_encaps_internal_wrapper(BinarySink *ciphertext, BinarySink *kout, + const mlkem_params *params, ptrlen ek, + ptrlen m) +{ + assert(m.len == 32 && "Invalid m length"); + return mlkem_encaps_internal(ciphertext, kout, params, ek, m.ptr); +} + strbuf *rsa_ssh1_encrypt_wrapper(ptrlen input, RSAKey *key) { /* Fold the boolean return value in C into the string return value diff --git a/test/testcrypt.py b/test/testcrypt.py index 217dbf35..a71bd394 100644 --- a/test/testcrypt.py +++ b/test/testcrypt.py @@ -182,7 +182,7 @@ def make_argword(arg, argtype, fnname, argindex, argname, to_preserve): if typename in { "hashalg", "macalg", "keyalg", "cipheralg", "dh_group", "ecdh_alg", "rsaorder", "primegenpolicy", - "argon2flavour", "fptype", "httpdigesthash"}: + "argon2flavour", "fptype", "httpdigesthash", "mlkem_params"}: arg = coerce_to_bytes(arg) if isinstance(arg, bytes) and b" " not in arg: dictkey = (typename, arg) diff --git a/test/testsc.c b/test/testsc.c index 00421a67..b4950d8d 100644 --- a/test/testsc.c +++ b/test/testsc.c @@ -82,6 +82,7 @@ #include "mpint.h" #include "crypto/ecc.h" #include "crypto/ntru.h" +#include "crypto/mlkem.h" static NORETURN PRINTF_LIKE(1, 2) void fatal_error(const char *p, ...) { @@ -431,6 +432,9 @@ VOLATILE_WRAPPED_DEFN(static, size_t, looplimit, (size_t x)) X(argon2) \ X(primegen_probabilistic) \ X(ntru) \ + X(mlkem512) \ + X(mlkem768) \ + X(mlkem1024) \ X(rfc6979_setup) \ X(rfc6979_attempt) \ /* end of list */ @@ -1745,6 +1749,60 @@ static void test_ntru(void) strbuf_free(buffer); } +static void test_mlkem(const mlkem_params *params) +{ + char rho[32], sigma[32], z[32], m[32], ek[1568], dk[3168], c[1568]; + char k[32], k2[32]; + + /* rho is a random but public value, so side channels are allowed + * to reveal it (and undoubtedly will). So we don't vary it + * between runs. */ + random_read(rho, 32); + + for (size_t i = 0; i < looplimit(32); i++) { + random_advance_counter(); + random_read(sigma, 32); + random_read(z, 32); + random_read(m, 32); + + log_start(); + + /* Every other iteration, tamper with the ciphertext so that + * implicit rejection occurs, because we need to test that + * that too is done in constant time. */ + unsigned tampering = i & 1; + + buffer_sink ek_sink[1]; buffer_sink_init(ek_sink, ek, sizeof(ek)); + buffer_sink dk_sink[1]; buffer_sink_init(dk_sink, dk, sizeof(dk)); + buffer_sink c_sink[1]; buffer_sink_init(c_sink, c, sizeof(c)); + buffer_sink k_sink[1]; buffer_sink_init(k_sink, k, sizeof(k)); + mlkem_keygen_rho_sigma( + BinarySink_UPCAST(ek_sink), BinarySink_UPCAST(dk_sink), + params, rho, sigma, z); + ptrlen ek_pl = make_ptrlen(ek, ek_sink->out - ek); + ptrlen dk_pl = make_ptrlen(dk, dk_sink->out - dk); + mlkem_encaps_internal( + BinarySink_UPCAST(c_sink), BinarySink_UPCAST(k_sink), + params, ek_pl, m); + dk[0] ^= tampering; + ptrlen c_pl = make_ptrlen(c, c_sink->out - c); + buffer_sink_init(k_sink, k2, sizeof(k2)); + bool success = mlkem_decaps( + BinarySink_UPCAST(k_sink), params, dk_pl, c_pl); + + log_end(); + + assert(success); + unsigned eq_expected = tampering ^ 1; + unsigned eq = smemeq(k, k2, 32); + assert(eq == eq_expected); + } +} + +static void test_mlkem512(void) { test_mlkem(&mlkem_params_512); } +static void test_mlkem768(void) { test_mlkem(&mlkem_params_768); } +static void test_mlkem1024(void) { test_mlkem(&mlkem_params_1024); } + static void test_rfc6979_setup(void) { mp_int *q = mp_new(512); From a3f22a2cf9620de4e8d53ce3e7c9485c526eafce Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Sun, 8 Dec 2024 10:34:10 +0000 Subject: [PATCH 17/44] Use the new 'HYBRID' names for the hybrid KEX packets. draft-kampanakis-curdle-ssh-pq-ke defines the packet names SSH_MSG_KEX_HYBRID_INIT and SSH_MSG_KEX_HYBRID_REPLY. They have the same numbers as ECDH_INIT and ECDH_REPLY, and don't change anything else, so this is just a naming change. But I think it's a good one, because the post-quantum KEMs are less symmetric than ECDH (they're much more like Ben's RSA kex in concept, though very different in detail), and shouldn't try to pretend they're the same kind of thing. Also this enables logparse.pl to give a warning about the fact that one string in each packet contains two separate keys glomphed together. For the latter reason (and also because it's easier in my code structure) I've also switched to using the HYBRID naming for the existing NTRU + Curve25519 hybrid method, even though the Internet-Draft for that one still uses the ECDH names. Sorry, but I think it's clearer! --- contrib/logparse.pl | 40 ++++++++++++++++++++++++++++++++++++++++ crypto/ecc-ssh.c | 2 ++ crypto/kex-hybrid.c | 3 +++ ssh.h | 9 +++++++++ ssh/kex2-client.c | 2 +- 5 files changed, 55 insertions(+), 1 deletion(-) diff --git a/contrib/logparse.pl b/contrib/logparse.pl index eb429302..034aa72d 100755 --- a/contrib/logparse.pl +++ b/contrib/logparse.pl @@ -172,6 +172,16 @@ sub usage { my ($direction, $seq, $data) = @_; print "\n"; }, +#define SSH2_MSG_KEX_HYBRID_INIT 30 /* 0x1e */ + 'SSH2_MSG_KEX_HYBRID_INIT' => sub { + my ($direction, $seq, $data) = @_; + print "\n"; + }, +#define SSH2_MSG_KEX_HYBRID_REPLY 31 /* 0x1f */ + 'SSH2_MSG_KEX_HYBRID_REPLY' => sub { + my ($direction, $seq, $data) = @_; + print "\n"; + }, #define SSH2_MSG_USERAUTH_REQUEST 50 /* 0x32 */ 'SSH2_MSG_USERAUTH_REQUEST' => sub { my ($direction, $seq, $data) = @_; @@ -657,6 +667,16 @@ sub usage { # curve is. So the best we can do is just dump the raw data. printf " client public value: %s\n", (unpack "H*", $cpv); }, + 'SSH2_MSG_KEX_HYBRID_INIT' => sub { + my ($data) = @_; + my ($cpv) = &parse("s", $data); + # Hybrid post-quantum + classical KEX is even more confusing, + # since two separate pieces of data are glomphed together into + # this string without any obvious dividing line. The best we + # can sensibly do is to announce that in the log. + printf " client PQ encryption key + public ECDH value: %s\n", + (unpack "H*", $cpv); + }, 'SSH2_MSG_KEXDH_REPLY' => sub { my ($data) = @_; my ($hostkeyblob, $f, $sigblob) = &parse("sms", $data); @@ -708,6 +728,26 @@ sub usage { printf " $key: $value\n"; } }, + 'SSH2_MSG_KEX_HYBRID_REPLY' => sub { + my ($data) = @_; + my ($hostkeyblob, $spv, $sigblob) = &parse("sss", $data); + my ($hktype, @hostkey) = &parse_public_key($hostkeyblob); + printf " host key: %s\n", $hktype; + while (@hostkey) { + my ($key, $value) = splice @hostkey, 0, 2; + printf " $key: $value\n"; + } + # Similarly to HYBRID_INIT, warn the reader that this string + # contains two separate things glomphed together + printf " server PQ KEM ciphertext + public ECDH value: %s\n", + (unpack "H*", $spv); + printf " signature:\n"; + my @signature = &parse_signature($sigblob, $hktype); + while (@signature) { + my ($key, $value) = splice @signature, 0, 2; + printf " $key: $value\n"; + } + }, 'SSH2_MSG_NEWKEYS' => sub {}, 'SSH2_MSG_SERVICE_REQUEST' => sub { my ($data) = @_; diff --git a/crypto/ecc-ssh.c b/crypto/ecc-ssh.c index ca712c31..e524dfc4 100644 --- a/crypto/ecc-ssh.c +++ b/crypto/ecc-ssh.c @@ -1615,6 +1615,7 @@ static const ecdh_keyalg ssh_ecdhkex_m_alg = { .getpublic = ssh_ecdhkex_m_getpublic, .getkey = ssh_ecdhkex_m_getkey, .description = ssh_ecdhkex_description, + .packet_naming_ctx = SSH2_PKTCTX_ECDHKEX, }; const ssh_kex ssh_ec_kex_curve25519 = { .name = "curve25519-sha256", @@ -1655,6 +1656,7 @@ static const ecdh_keyalg ssh_ecdhkex_w_alg = { .getpublic = ssh_ecdhkex_w_getpublic, .getkey = ssh_ecdhkex_w_getkey, .description = ssh_ecdhkex_description, + .packet_naming_ctx = SSH2_PKTCTX_ECDHKEX, }; static const struct eckex_extra kex_extra_nistp256 = { ec_p256 }; const ssh_kex ssh_ec_kex_nistp256 = { diff --git a/crypto/kex-hybrid.c b/crypto/kex-hybrid.c index e7291342..e0c78743 100644 --- a/crypto/kex-hybrid.c +++ b/crypto/kex-hybrid.c @@ -169,6 +169,7 @@ static const ecdh_keyalg hybrid_client_vt = { .getpublic = hybrid_client_getpublic, .getkey = hybrid_client_getkey, .description = hybrid_description, + .packet_naming_ctx = SSH2_PKTCTX_HYBRIDKEX, }; /* ---------------------------------------------------------------------- @@ -267,6 +268,7 @@ static const ecdh_keyalg hybrid_server_vt = { .getkey = hybrid_server_getkey, .getpublic = hybrid_server_getpublic, .description = hybrid_description, + .packet_naming_ctx = SSH2_PKTCTX_HYBRIDKEX, }; /* ---------------------------------------------------------------------- @@ -287,6 +289,7 @@ static const ecdh_keyalg hybrid_selector_vt = { * functions that don't require an instance. */ .new = hybrid_selector_new, .description = hybrid_description, + .packet_naming_ctx = SSH2_PKTCTX_HYBRIDKEX, }; /* ---------------------------------------------------------------------- diff --git a/ssh.h b/ssh.h index dcd111a9..0dd3e6c0 100644 --- a/ssh.h +++ b/ssh.h @@ -136,6 +136,7 @@ typedef enum { SSH2_PKTCTX_DHGROUP, SSH2_PKTCTX_DHGEX, SSH2_PKTCTX_ECDHKEX, + SSH2_PKTCTX_HYBRIDKEX, SSH2_PKTCTX_GSSKEX, SSH2_PKTCTX_RSAKEX } Pkt_KCtx; @@ -992,6 +993,12 @@ struct ecdh_keyalg { void (*getpublic)(ecdh_key *key, BinarySink *bs); bool (*getkey)(ecdh_key *key, ptrlen remoteKey, BinarySink *bs); char *(*description)(const ssh_kex *kex); + + /* Some things that use this vtable are genuinely elliptic-curve + * Diffie-Hellman. Others are hybrid PQ + classical kex methods. + * Provide a packet-naming context for use in the SSH log. (Purely + * cosmetic.) */ + Pkt_KCtx packet_naming_ctx; }; static inline ecdh_key *ecdh_key_new(const ssh_kex *kex, bool is_server) { return kex->ecdh_vt->new(kex, is_server); } @@ -1788,6 +1795,8 @@ void platform_ssh_share_cleanup(const char *name); K(y, SSH2_MSG_KEXRSA_DONE, 32, SSH2_PKTCTX_RSAKEX) \ K(y, SSH2_MSG_KEX_ECDH_INIT, 30, SSH2_PKTCTX_ECDHKEX) \ K(y, SSH2_MSG_KEX_ECDH_REPLY, 31, SSH2_PKTCTX_ECDHKEX) \ + K(y, SSH2_MSG_KEX_HYBRID_INIT, 30, SSH2_PKTCTX_HYBRIDKEX) \ + K(y, SSH2_MSG_KEX_HYBRID_REPLY, 31, SSH2_PKTCTX_HYBRIDKEX) \ X(y, SSH2_MSG_USERAUTH_REQUEST, 50) \ X(y, SSH2_MSG_USERAUTH_FAILURE, 51) \ X(y, SSH2_MSG_USERAUTH_SUCCESS, 52) \ diff --git a/ssh/kex2-client.c b/ssh/kex2-client.c index d5425237..108cf549 100644 --- a/ssh/kex2-client.c +++ b/ssh/kex2-client.c @@ -190,7 +190,7 @@ void ssh2kex_coroutine(struct ssh2_transport_state *s, bool *aborted) ssh_hash_alg(s->exhash)->text_name); sfree(desc); - s->ppl.bpp->pls->kctx = SSH2_PKTCTX_ECDHKEX; + s->ppl.bpp->pls->kctx = s->kex_alg->ecdh_vt->packet_naming_ctx; s->ecdh_key = ecdh_key_new(s->kex_alg, false); From 3c6a51390620926e50c0617e422819f5a804defa Mon Sep 17 00:00:00 2001 From: Jacob Nevins Date: Sun, 8 Dec 2024 11:41:02 +0000 Subject: [PATCH 18/44] Minimally document ML-KEM key exchange methods. --- doc/config.but | 6 ++++++ doc/index.but | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/doc/config.but b/doc/config.but index a28e866c..fb836ec2 100644 --- a/doc/config.but +++ b/doc/config.but @@ -2385,6 +2385,12 @@ Curve25519-based method (one of those included in \q{ECDH}), in such a way that it should be no \e{less} secure than that commonly-used method, and hopefully also resistant to a new class of attacks. +\b \q{ML-KEM / Curve25519 hybrid} and \q{ML-KEM NIST ECDH hybrid}: +similar hybrid constructs of \i{ML-KEM}, another lattice-based key +exchange method intended to be \i{quantum-resistant}. In the former, +ML-KEM is hybridised with Curve25519; in the latter, with NIST P384 +or P256. + \b \q{\i{ECDH}}: elliptic curve Diffie-Hellman key exchange, with a variety of standard curves and hash algorithms. diff --git a/doc/index.but b/doc/index.but index 35c479ff..94e2a477 100644 --- a/doc/index.but +++ b/doc/index.but @@ -699,7 +699,8 @@ saved sessions from \IM{Streamlined NTRU Prime} Streamlined NTRU Prime \IM{Streamlined NTRU Prime} NTRU Prime -\IM{quantum attacks} quantum attacks, resistance to +\IM{quantum attacks}{quantum-resistant} quantum attacks, resistance to +\IM{quantum attacks}{quantum-resistant} post-quantum algorithm \IM{repeat key exchange} repeat key exchange \IM{repeat key exchange} key exchange, repeat From 897ecf46784571640f0f62bd8bd9a8673805dfea Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Wed, 11 Dec 2024 18:20:58 +0000 Subject: [PATCH 19/44] SUPDUP: make the TDCRL command clear to end of line. A user reported shortly after 0.82 was released that they were experiencing display corruption when connecting to a PDP-10 running ITS using PuTTY's SUPDUP backend, and that the nature of the corruption was consistent with a missing clear-to-EOL operation. Without the SUPDUP or ITS expertise to debug it ourselves, we are indebted to Scott Michel for identifying where, and providing a patch. (However, now that the patch is presented, it's obvious even to me that a line should be cleared here! The comment in PuTTY's own code mentions clearing the line that the cursor has moved on to, and the same text appears in RFC 734.) --- otherbackends/supdup.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/otherbackends/supdup.c b/otherbackends/supdup.c index 6f574c9f..f9680f30 100644 --- a/otherbackends/supdup.c +++ b/otherbackends/supdup.c @@ -339,7 +339,7 @@ static void do_argsdone(Supdup *supdup, strbuf *outbuf, int c) that line. If the cursor is at the bottom line, scroll up. */ - put_fmt(outbuf, "\015\012"); + put_fmt(outbuf, "\015\012\033[K"); break; case TDNOP: From 1ef0fbaafcb5378525376fd5961bf72918846f4e Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Fri, 13 Dec 2024 19:19:02 +0000 Subject: [PATCH 20/44] Add helper function dupwcscat(). The wide-string version of dupcat(), with an identical wrapper macro to automatically append a correctly typed NULL. --- misc.h | 2 ++ utils/CMakeLists.txt | 1 + utils/dupwcscat.c | 48 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+) create mode 100644 utils/dupwcscat.c diff --git a/misc.h b/misc.h index 7d94e7e6..c64eb01b 100644 --- a/misc.h +++ b/misc.h @@ -29,6 +29,8 @@ char *dupstr(const char *s); wchar_t *dupwcs(const wchar_t *s); char *dupcat_fn(const char *s1, ...); #define dupcat(...) dupcat_fn(__VA_ARGS__, (const char *)NULL) +wchar_t *dupwcscat_fn(const wchar_t *s1, ...); +#define dupwcscat(...) dupwcscat_fn(__VA_ARGS__, (const wchar_t *)NULL) char *dupprintf(const char *fmt, ...) PRINTF_LIKE(1, 2); char *dupvprintf(const char *fmt, va_list ap); void burnstr(char *string); diff --git a/utils/CMakeLists.txt b/utils/CMakeLists.txt index 7accda5f..2581559e 100644 --- a/utils/CMakeLists.txt +++ b/utils/CMakeLists.txt @@ -29,6 +29,7 @@ add_sources_from_current_dir(utils dupprintf.c dupstr.c dupwcs.c + dupwcscat.c dup_mb_to_wc.c dup_wc_to_mb.c encode_utf8.c diff --git a/utils/dupwcscat.c b/utils/dupwcscat.c new file mode 100644 index 00000000..4b9ed5a9 --- /dev/null +++ b/utils/dupwcscat.c @@ -0,0 +1,48 @@ +/* + * Implementation function behind dupwcscat() in misc.h. + * + * This function is called with an arbitrary number of 'const wchar_t + * *' parameters, of which the last one is a null pointer. The wrapper + * macro puts on the null pointer itself, so normally callers don't + * have to. + */ + +#include +#include + +#include "defs.h" +#include "misc.h" + +wchar_t *dupwcscat_fn(const wchar_t *s1, ...) +{ + int len; + wchar_t *p, *q, *sn; + va_list ap; + + len = wcslen(s1); + va_start(ap, s1); + while (1) { + sn = va_arg(ap, wchar_t *); + if (!sn) + break; + len += wcslen(sn); + } + va_end(ap); + + p = snewn(len + 1, wchar_t); + wcscpy(p, s1); + q = p + wcslen(p); + + va_start(ap, s1); + while (1) { + sn = va_arg(ap, wchar_t *); + if (!sn) + break; + wcscpy(q, sn); + q += wcslen(q); + } + va_end(ap); + + return p; +} + From 22dfc46fb2b3159f1d7a0bdc7b85dad841ca020c Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Fri, 13 Dec 2024 19:19:08 +0000 Subject: [PATCH 21/44] Windows: add filename_to_wstr(). The wide-string version of filename_to_str(): given a Filename, return a reference to its contained wchar_t string form. --- windows/platform.h | 1 + windows/utils/filename.c | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/windows/platform.h b/windows/platform.h index 7e2227b7..2af9b8c4 100644 --- a/windows/platform.h +++ b/windows/platform.h @@ -66,6 +66,7 @@ struct Filename { char *cpath, *utf8path; }; Filename *filename_from_wstr(const wchar_t *str); +const wchar_t *filename_to_wstr(const Filename *fn); FILE *f_open(const Filename *filename, const char *mode, bool isprivate); #ifndef SUPERSEDE_FONTSPEC_FOR_TESTING diff --git a/windows/utils/filename.c b/windows/utils/filename.c index 8f4c53af..fd494869 100644 --- a/windows/utils/filename.c +++ b/windows/utils/filename.c @@ -47,6 +47,11 @@ const char *filename_to_str(const Filename *fn) return fn->cpath; /* FIXME */ } +const wchar_t *filename_to_wstr(const Filename *fn) +{ + return fn->wpath; +} + bool filename_equal(const Filename *f1, const Filename *f2) { /* wpath is primary: two filenames refer to the same file if they From f8e1a2b3a934d750aba7c26d182f52d71952c529 Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Fri, 13 Dec 2024 19:23:30 +0000 Subject: [PATCH 22/44] Windows: rewrite request_file() to support Unicode. This centralises into windows/utils/request_file.c all of the code that deals with the OPENFILENAME structure, and decides centrally whether to use the Unicode or ANSI version of that structure and its associated APIs. Now the output of any request_file function is our own 'Filename' abstract type, instead of a raw char or wchar_t buffer, which means that _any_ file dialog can produce a full Unicode filename if the user wants to select one - and yet, in the w32old build, they all uniformly fall back to the ANSI version, which is the only one that works at all pre-NT. A side effect: I've turned the FILTER_FOO_FILES family of definitions from platform-specific #defines into a reasonably sensible enum. This didn't affect the GTK side of things , because I'd never got round to figuring out how to filter a file dialog down to a subset of files in GTK, and still haven't. So I've just moved the existing FIXME comment from platform.h to dialog.c. --- config.c | 4 +- dialog.c | 2 +- dialog.h | 17 +- putty.h | 15 ++ ssh/ca-config.c | 7 +- unix/dialog.c | 3 + unix/platform.h | 4 - windows/config.c | 4 +- windows/controls.c | 49 ++-- windows/pageant.c | 55 ++--- windows/platform.h | 42 ++-- windows/puttygen.c | 80 +++---- windows/utils/request_file.c | 418 ++++++++++++++++++++++++++++------- 13 files changed, 444 insertions(+), 256 deletions(-) diff --git a/config.c b/config.c index f2272068..a94a4cca 100644 --- a/config.c +++ b/config.c @@ -1987,7 +1987,7 @@ void setup_config_box(struct controlbox *b, bool midsession, sshrawlogname, 'r', I(LGTYP_SSHRAW)); } ctrl_filesel(s, "Log file name:", 'f', - NULL, true, "Select session log file name", + FILTER_ALL_FILES, true, "Select session log file name", HELPCTX(logging_filename), conf_filesel_handler, I(CONF_logfilename)); ctrl_text(s, "(Log file name can contain &Y, &M, &D for date," @@ -2932,7 +2932,7 @@ void setup_config_box(struct controlbox *b, bool midsession, conf_filesel_handler, I(CONF_keyfile)); ctrl_filesel(s, "Certificate to use with the private key " "(optional):", 'e', - NULL, false, "Select certificate file", + FILTER_ALL_FILES, false, "Select certificate file", HELPCTX(ssh_auth_cert), conf_filesel_handler, I(CONF_detached_cert)); diff --git a/dialog.c b/dialog.c index 511331d3..0ad34d25 100644 --- a/dialog.c +++ b/dialog.c @@ -392,7 +392,7 @@ dlgcontrol *ctrl_draglist(struct controlset *s, const char *label, } dlgcontrol *ctrl_filesel(struct controlset *s, const char *label, - char shortcut, FILESELECT_FILTER_TYPE filter, + char shortcut, FilereqFilter filter, bool write, const char *title, HelpCtx helpctx, handler_fn handler, intorptr context) { diff --git a/dialog.h b/dialog.h index 19e0695d..4e8e2ae1 100644 --- a/dialog.h +++ b/dialog.h @@ -348,21 +348,8 @@ struct dlgcontrol { * files the file selector would do well to only show .PPK * files (on those systems where this is the chosen * extension). - * - * The precise contents of `filter' are platform-defined, - * unfortunately. The special value NULL means `all files' - * and is always a valid fallback. - * - * Unlike almost all strings in this structure, this value - * is NOT expected to require freeing (although of course - * you can always use ctrl_alloc if you do need to create - * one on the fly). This is because the likely mode of use - * is to define string constants in a platform-specific - * header file, and directly reference those. Or worse, a - * particular platform might choose to cast integers into - * this pointer type... */ - FILESELECT_FILTER_TYPE filter; + FilereqFilter filter; /* * Some systems like to know whether a file selector is * choosing a file to read or one to write (and possibly @@ -552,7 +539,7 @@ dlgcontrol *ctrl_draglist(struct controlset *, const char *label, char shortcut, HelpCtx helpctx, handler_fn handler, intorptr context); dlgcontrol *ctrl_filesel(struct controlset *, const char *label, - char shortcut, FILESELECT_FILTER_TYPE filter, + char shortcut, FilereqFilter filter, bool write, const char *title, HelpCtx helpctx, handler_fn handler, intorptr context); dlgcontrol *ctrl_fontsel(struct controlset *, const char *label, diff --git a/putty.h b/putty.h index bd0160fe..fdcd6ca3 100644 --- a/putty.h +++ b/putty.h @@ -4,6 +4,21 @@ #include /* for wchar_t */ #include /* for INT_MAX */ +/* + * Declared before including platform.h, because that will refer to it + * + * An enum for different types of file that a GUI file requester might + * focus on. (Our requesters never _insist_ on a particular file type + * or extension - there's always an escape hatch to select any file + * you want - but the default can be configured.) + */ +typedef enum { + FILTER_ALL_FILES, /* no particular focus */ + FILTER_KEY_FILES, /* .ppk */ + FILTER_DYNLIB_FILES, /* whatever the host platform uses as shared libs */ + FILTER_SOUND_FILES, /* whatever kind of sound file we can use as bell */ +} FilereqFilter; + #include "defs.h" #include "platform.h" #include "network.h" diff --git a/ssh/ca-config.c b/ssh/ca-config.c index 8c180b36..ff4c66dc 100644 --- a/ssh/ca-config.c +++ b/ssh/ca-config.c @@ -453,9 +453,10 @@ void setup_ca_config_box(struct controlbox *b) P(st), P(NULL)); c->column = 0; st->ca_pubkey_edit = c; - c = ctrl_filesel(s, "Read from file", NO_SHORTCUT, NULL, false, - "Select public key file of certification authority", - HELPCTX(ssh_kex_cert), ca_pubkey_file_handler, P(st)); + c = ctrl_filesel( + s, "Read from file", NO_SHORTCUT, FILTER_ALL_FILES, false, + "Select public key file of certification authority", + HELPCTX(ssh_kex_cert), ca_pubkey_file_handler, P(st)); c->fileselect.just_button = true; c->align_next_to = st->ca_pubkey_edit; c->column = 1; diff --git a/unix/dialog.c b/unix/dialog.c index fa645b3a..0838ff03 100644 --- a/unix/dialog.c +++ b/unix/dialog.c @@ -1739,6 +1739,9 @@ static void filefont_clicked(GtkButton *button, gpointer data) struct uctrl *uc = dlg_find_bywidget(dp, GTK_WIDGET(button)); if (uc->ctrl->type == CTRL_FILESELECT) { + /* + * FIXME: do something about uc->ctrl->fileselect.filter + */ #ifdef USE_GTK_FILE_CHOOSER_DIALOG GtkWidget *filechoose = gtk_file_chooser_dialog_new( uc->ctrl->fileselect.title, GTK_WINDOW(dp->window), diff --git a/unix/platform.h b/unix/platform.h index 44c7986c..670febd7 100644 --- a/unix/platform.h +++ b/unix/platform.h @@ -84,10 +84,6 @@ typedef void *HelpCtx; #define NULL_HELPCTX ((HelpCtx)NULL) #define HELPCTX(x) NULL -typedef const char *FILESELECT_FILTER_TYPE; -#define FILTER_KEY_FILES NULL /* FIXME */ -#define FILTER_DYNLIB_FILES NULL /* FIXME */ - /* * Under X, selection data must not be NUL-terminated. */ diff --git a/windows/config.c b/windows/config.c index fc9070bf..732f3d40 100644 --- a/windows/config.c +++ b/windows/config.c @@ -159,7 +159,7 @@ void win_setup_config_box(struct controlbox *b, HWND *hwndp, bool has_help, } } ctrl_filesel(s, "Custom sound file to play as a bell:", NO_SHORTCUT, - FILTER_WAVE_FILES, false, "Select bell sound file", + FILTER_SOUND_FILES, false, "Select bell sound file", HELPCTX(bell_style), conf_filesel_handler, I(CONF_bell_wavefile)); @@ -377,7 +377,7 @@ void win_setup_config_box(struct controlbox *b, HWND *hwndp, bool has_help, if (!midsession && backend_vt_from_proto(PROT_SSH)) { s = ctrl_getset(b, "Connection/SSH/X11", "x11", "X11 forwarding"); ctrl_filesel(s, "X authority file for local display", 't', - NULL, false, "Select X authority file", + FILTER_ALL_FILES, false, "Select X authority file", HELPCTX(ssh_tunnels_xauthority), conf_filesel_handler, I(CONF_xauthfile)); } diff --git a/windows/controls.c b/windows/controls.c index b4c838b4..e1fcc589 100644 --- a/windows/controls.c +++ b/windows/controls.c @@ -1988,46 +1988,33 @@ bool winctrl_handle_command(struct dlgparam *dp, UINT msg, (msg == WM_COMMAND && (HIWORD(wParam) == BN_CLICKED || HIWORD(wParam) == BN_DOUBLECLICKED))) { - OPENFILENAMEW of; - wchar_t filename[FILENAME_MAX]; - - wchar_t *title_to_free = NULL; - - memset(&of, 0, sizeof(of)); - of.hwndOwner = dp->hwnd; - if (ctrl->fileselect.filter) - of.lpstrFilter = ctrl->fileselect.filter; - else - of.lpstrFilter = L"All Files (*.*)\0*\0\0\0"; - of.lpstrCustomFilter = NULL; - of.nFilterIndex = 1; - of.lpstrFile = filename; + Filename *fn_prev = NULL; if (!ctrl->fileselect.just_button) { - GetDlgItemTextW(dp->hwnd, c->base_id+1, - filename, lenof(filename)); - filename[lenof(filename)-1] = L'\0'; - } else { - *filename = L'\0'; + wchar_t *text = GetDlgItemTextW_alloc(dp->hwnd, c->base_id+1); + if (*text) + fn_prev = filename_from_wstr(text); + sfree(text); } - of.nMaxFile = lenof(filename); - of.lpstrFileTitle = NULL; - of.lpstrTitle = title_to_free = dup_mb_to_wc( - DEFAULT_CODEPAGE, ctrl->fileselect.title); - of.Flags = 0; - if (request_file_w(NULL, &of, false, - ctrl->fileselect.for_writing)) { + + Filename *fn = request_file( + dp->hwnd, ctrl->fileselect.title, fn_prev, + ctrl->fileselect.for_writing, NULL, false, + ctrl->fileselect.filter); + if (fn_prev) + filename_free(fn_prev); + + if (fn) { if (!ctrl->fileselect.just_button) { - SetDlgItemTextW(dp->hwnd, c->base_id + 1, filename); + SetDlgItemTextW(dp->hwnd, c->base_id + 1, + filename_to_wstr(fn)); ctrl->handler(ctrl, dp, dp->data, EVENT_VALCHANGE); } else { assert(!c->data); - c->data = filename; + c->data = fn; ctrl->handler(ctrl, dp, dp->data, EVENT_ACTION); c->data = NULL; } } - - sfree(title_to_free); } break; case CTRL_FONTSELECT: @@ -2433,7 +2420,7 @@ Filename *dlg_filesel_get(dlgcontrol *ctrl, dlgparam *dp) sfree(tmp); return ret; } else { - return filename_from_str(c->data); + return filename_copy(c->data); } } diff --git a/windows/pageant.c b/windows/pageant.c index eb858a3b..202a10f8 100644 --- a/windows/pageant.c +++ b/windows/pageant.c @@ -52,7 +52,7 @@ static char *putty_path; static bool restrict_putty_acl = false; /* CWD for "add key" file requester. */ -static filereq *keypath = NULL; +static filereq_saved_dir *keypath = NULL; /* From MSDN: In the WM_SYSCOMMAND message, the four low-order bits of * wParam are used by Windows, and should be masked off, so we shouldn't @@ -543,49 +543,21 @@ static void win_add_keyfile(Filename *filename, bool encrypted) */ static void prompt_add_keyfile(bool encrypted) { - OPENFILENAME of; - char *filelist = snewn(8192, char); - - if (!keypath) keypath = filereq_new(); - memset(&of, 0, sizeof(of)); - of.hwndOwner = traywindow; - of.lpstrFilter = FILTER_KEY_FILES_C; - of.lpstrCustomFilter = NULL; - of.nFilterIndex = 1; - of.lpstrFile = filelist; - *filelist = '\0'; - of.nMaxFile = 8192; - of.lpstrFileTitle = NULL; - of.lpstrTitle = "Select Private Key File"; - of.Flags = OFN_ALLOWMULTISELECT | OFN_EXPLORER; - if (request_file(keypath, &of, true, false)) { - if (strlen(filelist) > of.nFileOffset) { - /* Only one filename returned? */ - Filename *fn = filename_from_str(filelist); - win_add_keyfile(fn, encrypted); - filename_free(fn); - } else { - /* we are returned a bunch of strings, end to - * end. first string is the directory, the - * rest the filenames. terminated with an - * empty string. - */ - char *dir = filelist; - char *filewalker = filelist + strlen(dir) + 1; - while (*filewalker != '\0') { - char *filename = dupcat(dir, "\\", filewalker); - Filename *fn = filename_from_str(filename); - win_add_keyfile(fn, encrypted); - filename_free(fn); - sfree(filename); - filewalker += strlen(filewalker) + 1; - } - } + if (!keypath) + keypath = filereq_saved_dir_new(); + + struct request_multi_file_return *rmf = request_multi_file( + traywindow, "Select Private Key File", NULL, false, + keypath, true, FILTER_KEY_FILES); + + if (rmf) { + for (size_t i = 0; i < rmf->nfilenames; i++) + win_add_keyfile(rmf->filenames[i], encrypted); + request_multi_file_free(rmf); keylist_update(); pageant_forget_passphrases(); } - sfree(filelist); } /* @@ -1957,7 +1929,8 @@ int WINAPI WinMain(HINSTANCE inst, HINSTANCE prev, LPSTR cmdline, int show) DestroyMenu(systray_menu); } - if (keypath) filereq_free(keypath); + if (keypath) + filereq_saved_dir_free(keypath); if (openssh_config_file) { /* diff --git a/windows/platform.h b/windows/platform.h index 2af9b8c4..10551e87 100644 --- a/windows/platform.h +++ b/windows/platform.h @@ -296,27 +296,6 @@ void write_aclip(HWND hwnd, int clipboard, char *, int); */ #define sk_getxdmdata(socket, lenp) (NULL) -/* - * File-selector filter strings used in the config box. On Windows, - * these strings are of exactly the type needed to go in - * `lpstrFilter' in an OPENFILENAME structure. - */ -typedef const wchar_t *FILESELECT_FILTER_TYPE; -#define FILTER_KEY_FILES (L"PuTTY Private Key Files (*.ppk)\0*.ppk\0" \ - L"All Files (*.*)\0*\0\0\0") -#define FILTER_WAVE_FILES (L"Wave Files (*.wav)\0*.WAV\0" \ - L"All Files (*.*)\0*\0\0\0") -#define FILTER_DYNLIB_FILES (L"Dynamic Library Files (*.dll)\0*.dll\0" \ - L"All Files (*.*)\0*\0\0\0") - -/* char-based versions of the above, for outlying uses of file selectors. */ -#define FILTER_KEY_FILES_C ("PuTTY Private Key Files (*.ppk)\0*.ppk\0" \ - "All Files (*.*)\0*\0\0\0") -#define FILTER_WAVE_FILES_C ("Wave Files (*.wav)\0*.WAV\0" \ - "All Files (*.*)\0*\0\0\0") -#define FILTER_DYNLIB_FILES_C ("Dynamic Library Files (*.dll)\0*.dll\0" \ - "All Files (*.*)\0*\0\0\0") - /* * Exports from network.c. */ @@ -417,12 +396,21 @@ void init_common_controls(void); /* also does some DLL-loading */ /* * Exports from utils. */ -typedef struct filereq_tag filereq; /* cwd for file requester */ -bool request_file(filereq *state, OPENFILENAME *of, bool preserve, bool save); -bool request_file_w(filereq *state, OPENFILENAMEW *of, - bool preserve, bool save); -filereq *filereq_new(void); -void filereq_free(filereq *state); +typedef struct filereq_saved_dir filereq_saved_dir; +filereq_saved_dir *filereq_saved_dir_new(void); +void filereq_saved_dir_free(filereq_saved_dir *state); +Filename *request_file( + HWND hwnd, const char *title, Filename *initial, bool save, + filereq_saved_dir *dir, bool preserve_cwd, FilereqFilter filter); +struct request_multi_file_return { + Filename **filenames; + size_t nfilenames; +}; +struct request_multi_file_return *request_multi_file( + HWND hwnd, const char *title, Filename *initial, bool save, + filereq_saved_dir *dir, bool preserve_cwd, FilereqFilter filter); +void request_multi_file_free(struct request_multi_file_return *); + void pgp_fingerprints_msgbox(HWND owner); int message_box(HWND owner, LPCTSTR text, LPCTSTR caption, DWORD style, bool utf8, DWORD helpctxid); diff --git a/windows/puttygen.c b/windows/puttygen.c index 0e4f9a78..c5115c06 100644 --- a/windows/puttygen.c +++ b/windows/puttygen.c @@ -451,34 +451,6 @@ static INT_PTR CALLBACK PPKParamsProc(HWND hwnd, UINT msg, return 0; } -/* - * Prompt for a key file. Assumes the filename buffer is of size - * FILENAME_MAX. - */ -static bool prompt_keyfile(HWND hwnd, char *dlgtitle, - char *filename, bool save, bool ppk) -{ - OPENFILENAME of; - memset(&of, 0, sizeof(of)); - of.hwndOwner = hwnd; - if (ppk) { - of.lpstrFilter = "PuTTY Private Key Files (*.ppk)\0*.ppk\0" - "All Files (*.*)\0*\0\0\0"; - of.lpstrDefExt = ".ppk"; - } else { - of.lpstrFilter = "All Files (*.*)\0*\0\0\0"; - } - of.lpstrCustomFilter = NULL; - of.nFilterIndex = 1; - of.lpstrFile = filename; - *filename = '\0'; - of.nMaxFile = FILENAME_MAX; - of.lpstrFileTitle = NULL; - of.lpstrTitle = dlgtitle; - of.Flags = 0; - return request_file(NULL, &of, false, save); -} - /* * Dialog-box function for the Licence box. */ @@ -2016,7 +1988,6 @@ static INT_PTR CALLBACK MainDlgProc(HWND hwnd, UINT msg, state = (struct MainDlgState *) GetWindowLongPtr(hwnd, GWLP_USERDATA); if (state->key_exists) { - char filename[FILENAME_MAX]; char *passphrase, *passphrase2; int type, realtype; @@ -2068,26 +2039,28 @@ static INT_PTR CALLBACK MainDlgProc(HWND hwnd, UINT msg, break; } } - if (prompt_keyfile(hwnd, "Save private key as:", - filename, true, (type == realtype))) { + Filename *fn = request_file( + hwnd, "Save private key as:", NULL, true, NULL, false, + (type==realtype ? FILTER_KEY_FILES : FILTER_ALL_FILES)); + if (fn) { int ret; - FILE *fp = fopen(filename, "r"); + FILE *fp = f_open(fn, "r", false); if (fp) { char *buffer; fclose(fp); buffer = dupprintf("Overwrite existing file\n%s?", - filename); + filename_to_str(fn)); ret = MessageBox(hwnd, buffer, "PuTTYgen Warning", MB_YESNO | MB_ICONWARNING); sfree(buffer); if (ret != IDYES) { burnstr(passphrase); + filename_free(fn); break; } } if (state->ssh2) { - Filename *fn = filename_from_str(filename); if (type != realtype) ret = export_ssh2(fn, type, &state->ssh2key, *passphrase ? passphrase : NULL); @@ -2095,21 +2068,19 @@ static INT_PTR CALLBACK MainDlgProc(HWND hwnd, UINT msg, ret = ppk_save_f(fn, &state->ssh2key, *passphrase ? passphrase : NULL, &save_params); - filename_free(fn); } else { - Filename *fn = filename_from_str(filename); if (type != realtype) ret = export_ssh1(fn, type, &state->key, *passphrase ? passphrase : NULL); else ret = rsa1_save_f(fn, &state->key, *passphrase ? passphrase : NULL); - filename_free(fn); } if (ret <= 0) { MessageBox(hwnd, "Unable to save key file", "PuTTYgen Error", MB_OK | MB_ICONERROR); } + filename_free(fn); } burnstr(passphrase); } @@ -2120,23 +2091,26 @@ static INT_PTR CALLBACK MainDlgProc(HWND hwnd, UINT msg, state = (struct MainDlgState *) GetWindowLongPtr(hwnd, GWLP_USERDATA); if (state->key_exists) { - char filename[FILENAME_MAX]; - if (prompt_keyfile(hwnd, "Save public key as:", - filename, true, false)) { + Filename *fn = request_file( + hwnd, "Save public key as:", NULL, true, NULL, false, + FILTER_ALL_FILES); + if (fn) { int ret; - FILE *fp = fopen(filename, "r"); + FILE *fp = f_open(fn, "r", false); if (fp) { char *buffer; fclose(fp); buffer = dupprintf("Overwrite existing file\n%s?", - filename); + filename_to_str(fn)); ret = MessageBox(hwnd, buffer, "PuTTYgen Warning", MB_YESNO | MB_ICONWARNING); sfree(buffer); - if (ret != IDYES) + if (ret != IDYES) { + filename_free(fn); break; + } } - fp = fopen(filename, "w"); + fp = f_open(fn, "w", false); if (!fp) { MessageBox(hwnd, "Unable to open key file", "PuTTYgen Error", MB_OK | MB_ICONERROR); @@ -2157,6 +2131,7 @@ static INT_PTR CALLBACK MainDlgProc(HWND hwnd, UINT msg, "PuTTYgen Error", MB_OK | MB_ICONERROR); } } + filename_free(fn); } } break; @@ -2167,10 +2142,11 @@ static INT_PTR CALLBACK MainDlgProc(HWND hwnd, UINT msg, state = (struct MainDlgState *) GetWindowLongPtr(hwnd, GWLP_USERDATA); if (!state->generation_thread_exists) { - char filename[FILENAME_MAX]; - if (prompt_keyfile(hwnd, "Load private key:", filename, false, - LOWORD(wParam) == IDC_LOAD)) { - Filename *fn = filename_from_str(filename); + Filename *fn = request_file( + hwnd, "Load private key:", NULL, false, NULL, false, + (LOWORD(wParam) == IDC_LOAD ? + FILTER_KEY_FILES : FILTER_ALL_FILES)); + if (fn) { load_key_file(hwnd, state, fn, LOWORD(wParam) != IDC_LOAD); filename_free(fn); } @@ -2182,10 +2158,10 @@ static INT_PTR CALLBACK MainDlgProc(HWND hwnd, UINT msg, state = (struct MainDlgState *) GetWindowLongPtr(hwnd, GWLP_USERDATA); if (state->key_exists && !state->generation_thread_exists) { - char filename[FILENAME_MAX]; - if (prompt_keyfile(hwnd, "Load certificate:", filename, false, - false)) { - Filename *fn = filename_from_str(filename); + Filename *fn = request_file( + hwnd, "Load certificate:", NULL, false, NULL, false, + FILTER_ALL_FILES); + if (fn) { add_certificate(hwnd, state, fn); filename_free(fn); } diff --git a/windows/utils/request_file.c b/windows/utils/request_file.c index 57363cad..e9a94291 100644 --- a/windows/utils/request_file.c +++ b/windows/utils/request_file.c @@ -1,114 +1,376 @@ -/* - * GetOpenFileName/GetSaveFileName tend to muck around with the process' - * working directory on at least some versions of Windows. - * Here's a wrapper that gives more control over this, and hides a little - * bit of other grottiness. - */ - #include "putty.h" -struct filereq_tag { - TCHAR cwd[MAX_PATH]; - WCHAR wcwd[MAX_PATH]; +typedef enum SavedDir { SD_NONE, SD_WCHAR, SD_CHAR } SavedDir; + +struct filereq_saved_dir { + SavedDir which; + union { + WCHAR wcwd[MAX_PATH]; + TCHAR cwd[MAX_PATH]; + }; }; +filereq_saved_dir *filereq_saved_dir_new(void) +{ + filereq_saved_dir *state = snew(filereq_saved_dir); + state->which = SD_NONE; + return state; +} + +void filereq_saved_dir_free(filereq_saved_dir *state) +{ + sfree(state); +} + +static void save_dir(filereq_saved_dir *state) +{ + DWORD dirlen; + + dirlen = GetCurrentDirectoryW(lenof(state->wcwd), state->wcwd); + if (dirlen > 0 && dirlen < lenof(state->wcwd)) { + state->which = SD_WCHAR; + return; + } + + dirlen = GetCurrentDirectoryA(lenof(state->cwd), state->cwd); + if (dirlen > 0 && dirlen < lenof(state->cwd)) { + state->which = SD_CHAR; + return; + } + + state->which = SD_NONE; +} + +static void restore_dir(filereq_saved_dir *state) +{ + switch (state->which) { + case SD_WCHAR: + SetCurrentDirectoryW(state->wcwd); + break; + case SD_CHAR: + SetCurrentDirectoryA(state->cwd); + break; + case SD_NONE: + break; + } +} + /* - * `of' is expected to be initialised with most interesting fields, but - * this function does some administrivia. (assume `of' was memset to 0) - * save==1 -> GetSaveFileName; save==0 -> GetOpenFileName - * `state' is optional. + * Internal function that brings up an ANSI-coded file dialog, + * returning a raw char * buffer containing the output. + * + * Inputs: + * - hwnd: the parent window for the dialog, or NULL if none + * - title: the window title + * - initial: a filename to populate the new dialog with, or NULL + * - dir: a location in which to persist the logical cwd used by + * successive file dialogs + * - save: true if the file dialog is for write rather than loading a file + * - filter: the default type of file being asked about, which will inform + * the choice of which files to display in the dialog, and also a default + * file extension for saving files + * - multi_filename_offset: NULL if you want to return exactly one file. + * Otherwise points to a size_t which gets nFileOffset from the result + * structure. This is passed to the request_multi_file_populate_* helpers + * below. + * - filename: buffer to put the output in + * - filename_size: size of the buffer. */ -bool request_file(filereq *state, OPENFILENAME *of, bool preserve, bool save) +static bool do_filereq_a( + HWND hwnd, const char *title, Filename *initial, filereq_saved_dir *dir, + bool save, FilereqFilter filter, size_t *multi_filename_offset, + char *filename, size_t filename_size) { - TCHAR cwd[MAX_PATH]; /* process CWD */ - bool ret; - - /* Get process CWD */ - if (preserve) { - DWORD r = GetCurrentDirectory(lenof(cwd), cwd); - if (r == 0 || r >= lenof(cwd)) - /* Didn't work, oh well. Stop trying to be clever. */ - preserve = false; - } + OPENFILENAMEA of; - /* Open the file requester, maybe setting lpstrInitialDir */ - { + memset(&of, 0, sizeof(of)); #ifdef OPENFILENAME_SIZE_VERSION_400 - of->lStructSize = OPENFILENAME_SIZE_VERSION_400; + of.lStructSize = OPENFILENAME_SIZE_VERSION_400; #else - of->lStructSize = sizeof(*of); + of.lStructSize = sizeof(of); #endif - of->lpstrInitialDir = (state && state->cwd[0]) ? state->cwd : NULL; - /* Actually put up the requester. */ - ret = save ? GetSaveFileName(of) : GetOpenFileName(of); + + if (dir && dir->which == SD_CHAR) + of.lpstrInitialDir = dir->cwd; + + switch (filter) { + default: /* FILTER_ALL_FILES */ + of.lpstrFilter = "All Files (*.*)\0*\0\0\0"; + break; + case FILTER_KEY_FILES: + of.lpstrFilter = "PuTTY Private Key Files (*.ppk)\0*.ppk\0" + "All Files (*.*)\0*\0\0\0"; + of.lpstrDefExt = ".ppk"; + break; + case FILTER_DYNLIB_FILES: + of.lpstrFilter = "Dynamic Library Files (*.dll)\0*.dll\0" + "All Files (*.*)\0*\0\0\0"; + of.lpstrDefExt = ".dll"; + break; + case FILTER_SOUND_FILES: + of.lpstrFilter = "Wave Files (*.wav)\0*.WAV\0" + "All Files (*.*)\0*\0\0\0"; + of.lpstrDefExt = ".wav"; + break; } + of.nFilterIndex = 1; - /* Get CWD left by requester */ - if (state) { - DWORD r = GetCurrentDirectory(lenof(state->cwd), state->cwd); - if (r == 0 || r >= lenof(state->cwd)) - /* Didn't work, oh well. */ - state->cwd[0] = '\0'; + of.hwndOwner = hwnd; + + if (initial) { + strncpy(filename, filename_to_str(initial), filename_size - 1); + filename[filename_size - 1] = '\0'; + } else { + *filename = '\0'; } + of.lpstrFile = filename; + of.nMaxFile = filename_size; + + of.lpstrTitle = title; + + if (multi_filename_offset) + of.Flags |= OFN_ALLOWMULTISELECT | OFN_EXPLORER; + + bool toret = save ? GetSaveFileNameA(&of) : GetOpenFileNameA(&of); + + if (dir) + save_dir(dir); - /* Restore process CWD */ - if (preserve) - /* If it fails, there's not much we can do. */ - (void) SetCurrentDirectory(cwd); + if (multi_filename_offset) + *multi_filename_offset = of.nFileOffset; - return ret; + return toret; } /* * Here's the same one again, the wide-string version */ -bool request_file_w(filereq *state, OPENFILENAMEW *of, - bool preserve, bool save) +static bool do_filereq_w( + HWND hwnd, const char *title, Filename *initial, filereq_saved_dir *dir, + bool save, FilereqFilter filter, size_t *multi_filename_offset, + wchar_t *filename, size_t filename_size) { - WCHAR cwd[MAX_PATH]; /* process CWD */ - bool ret; - - /* Get process CWD */ - if (preserve) { - DWORD r = GetCurrentDirectoryW(lenof(cwd), cwd); - if (r == 0 || r >= lenof(cwd)) - /* Didn't work, oh well. Stop trying to be clever. */ - preserve = false; + OPENFILENAMEW of; + void *tofree1 = NULL, *tofree2 = NULL; + + memset(&of, 0, sizeof(of)); + of.lStructSize = sizeof(of); + + if (dir && dir->which == SD_WCHAR) + of.lpstrInitialDir = dir->wcwd; + else if (dir && dir->which == SD_CHAR) { + wchar_t *winitdir = dup_mb_to_wc(CP_ACP, dir->cwd); + tofree1 = winitdir; + of.lpstrInitialDir = winitdir; } - /* Open the file requester, maybe setting lpstrInitialDir */ - { - of->lStructSize = sizeof(*of); - of->lpstrInitialDir = (state && state->wcwd[0]) ? state->wcwd : NULL; - /* Actually put up the requester. */ - ret = save ? GetSaveFileNameW(of) : GetOpenFileNameW(of); + switch (filter) { + default: /* FILTER_ALL_FILES */ + of.lpstrFilter = L"All Files (*.*)\0*\0\0\0"; + break; + case FILTER_KEY_FILES: + of.lpstrFilter = L"PuTTY Private Key Files (*.ppk)\0*.ppk\0" + "All Files (*.*)\0*\0\0\0"; + of.lpstrDefExt = L".ppk"; + break; + case FILTER_DYNLIB_FILES: + of.lpstrFilter = L"Dynamic Library Files (*.dll)\0*.dll\0" + "All Files (*.*)\0*\0\0\0"; + of.lpstrDefExt = L".dll"; + break; + case FILTER_SOUND_FILES: + of.lpstrFilter = L"Wave Files (*.wav)\0*.WAV\0" + "All Files (*.*)\0*\0\0\0"; + of.lpstrDefExt = L".wav"; + break; } + of.nFilterIndex = 1; - /* Get CWD left by requester */ - if (state) { - DWORD r = GetCurrentDirectoryW(lenof(state->wcwd), state->wcwd); - if (r == 0 || r >= lenof(state->wcwd)) - /* Didn't work, oh well. */ - state->wcwd[0] = L'\0'; + of.hwndOwner = hwnd; + + if (initial) { + wcsncpy(filename, filename_to_wstr(initial), filename_size - 1); + filename[filename_size - 1] = L'\0'; + } else { + *filename = L'\0'; } + of.lpstrFile = filename; + of.nMaxFile = filename_size; - /* Restore process CWD */ - if (preserve) - /* If it fails, there's not much we can do. */ - (void) SetCurrentDirectoryW(cwd); + if (title) { + wchar_t *wtitle = dup_mb_to_wc(CP_ACP, title); + tofree2 = wtitle; + of.lpstrTitle = wtitle; + } - return ret; + if (multi_filename_offset) + of.Flags |= OFN_ALLOWMULTISELECT | OFN_EXPLORER; + + bool toret = save ? GetSaveFileNameW(&of) : GetOpenFileNameW(&of); + + if (dir) + save_dir(dir); + + sfree(tofree1); + sfree(tofree2); + + if (multi_filename_offset) + *multi_filename_offset = of.nFileOffset; + + return toret; } -filereq *filereq_new(void) +Filename *request_file( + HWND hwnd, const char *title, Filename *initial, bool save, + filereq_saved_dir *dir, bool preserve_cwd, FilereqFilter filter) { - filereq *state = snew(filereq); - state->cwd[0] = '\0'; - state->wcwd[0] = L'\0'; - return state; + filereq_saved_dir saved_cwd[1]; + Filename *filename = NULL; + + if (preserve_cwd) + save_dir(saved_cwd); + + init_winver(); + if (osPlatformId != VER_PLATFORM_WIN32_NT) { + char namebuf[MAX_PATH]; + if (do_filereq_a( + hwnd, title, initial, dir, save, filter, + NULL, namebuf, sizeof(namebuf))) + filename = filename_from_str(namebuf); + } else { + wchar_t namebuf[MAX_PATH]; + if (do_filereq_w( + hwnd, title, initial, dir, save, filter, + NULL, namebuf, sizeof(namebuf))) + filename = filename_from_wstr(namebuf); + } + + if (preserve_cwd) + restore_dir(saved_cwd); + + return filename; } -void filereq_free(filereq *state) +static struct request_multi_file_return *request_multi_file_populate_a( + const char *buf, size_t first_filename_offset) { - sfree(state); + struct request_multi_file_return *rmf = + snew(struct request_multi_file_return); + + /* + * We expect one of two situations (guaranteed by the return from + * the OFN_MULTISELECT file dialog API function): + * + * 1. There is a single NUL-terminated filename string in buf, + * potentially including a path, and first_filename_offset points + * to the leaf name part of it. + * + * 2. There are multiple NUL-terminated strings in buf, with the + * first being a path, and the remaining ones being leaf names to + * concatenate to that path. An empty string / extra NUL + * terminates the whole list. first_filename_offset points to the + * start of the first leaf name. + * + * Hence, we can tell these apart by finding out whether a NUL + * appears in the buffer before first_filename_offset. If no, + * we're in case 1; if yes, case 2. + */ + if (strlen(buf) > first_filename_offset) { + /* Case 1: a single filename. */ + rmf->nfilenames = 1; + rmf->filenames = snewn(1, Filename *); + rmf->filenames[0] = filename_from_str(buf); + } else { + /* Case 2: multiple filenames preceded by a path. */ + size_t filenamesize = 16; + rmf->nfilenames = 0; + rmf->filenames = snewn(filenamesize, Filename *); + + const char *dir = buf; + const char *sep = + (*dir && dir[strlen(dir)-1] == '\\') ? "" : "\\"; + const char *p = buf + strlen(dir) + 1; + for (; *p; p += strlen(p) + 1) { + char *filename = dupcat(dir, sep, p); + sgrowarray(rmf->filenames, filenamesize, rmf->nfilenames); + rmf->filenames[rmf->nfilenames++] = filename_from_str(filename); + sfree(filename); + } + } + return rmf; +} + +/* + * Here's the same one again, the wide-string version + */ +static struct request_multi_file_return *request_multi_file_populate_w( + const wchar_t *buf, size_t first_filename_offset) +{ + struct request_multi_file_return *rmf = + snew(struct request_multi_file_return); + if (wcslen(buf) > first_filename_offset) { + rmf->nfilenames = 1; + rmf->filenames = snewn(1, Filename *); + rmf->filenames[0] = filename_from_wstr(buf); + } else { + size_t filenamesize = 16; + rmf->nfilenames = 0; + rmf->filenames = snewn(filenamesize, Filename *); + + const wchar_t *dir = buf; + const wchar_t *sep = + (*dir && dir[wcslen(dir)-1] == L'\\') ? L"" : L"\\"; + const wchar_t *p = buf + wcslen(dir) + 1; + for (; *p; p += wcslen(p) + 1) { + wchar_t *filename = dupwcscat(dir, sep, p); + sgrowarray(rmf->filenames, filenamesize, rmf->nfilenames); + rmf->filenames[rmf->nfilenames++] = filename_from_wstr(filename); + sfree(filename); + } + } + return rmf; +} + +#define MULTI_FACTOR 32 + +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) +{ + filereq_saved_dir saved_cwd[1]; + struct request_multi_file_return *rmf = NULL; + size_t first_filename_offset; + + if (preserve_cwd) + save_dir(saved_cwd); + + init_winver(); + if (osPlatformId != VER_PLATFORM_WIN32_NT) { + char namebuf[MAX_PATH * MULTI_FACTOR]; + if (do_filereq_a( + hwnd, title, initial, dir, save, filter, + &first_filename_offset, namebuf, sizeof(namebuf))) + rmf = request_multi_file_populate_a( + namebuf, first_filename_offset); + } else { + wchar_t namebuf[MAX_PATH * MULTI_FACTOR]; + if (do_filereq_w( + hwnd, title, initial, dir, save, filter, + &first_filename_offset, namebuf, sizeof(namebuf))) + rmf = request_multi_file_populate_w( + namebuf, first_filename_offset); + } + + if (preserve_cwd) + restore_dir(saved_cwd); + + return rmf; +} + +void request_multi_file_free(struct request_multi_file_return *rmf) +{ + for (size_t i = 0; i < rmf->nfilenames; i++) + filename_free(rmf->filenames[i]); + sfree(rmf->filenames); + sfree(rmf); } From edd5e13ffc976025443e0b9d75888249aa3325a9 Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Sat, 14 Dec 2024 11:44:28 +0000 Subject: [PATCH 23/44] Fix assertion failure on Restart Session. This occurred if the SSH server closed the connection for any reason (in practice usually a timeout, but reproducible more easily by manually killing a test server process) while the user was in the middle of any kind of interactive prompt-based login in the GUI PuTTY terminal (be it simple password, k-i, private key passphrase, whatever). The problem was that term->userpass_state wasn't cleaned up when the connection died, and then if you started a fresh SSH session in the same terminal, the attempt to create a new term->userpass_state would find there was one already there. The simplest place to insert the missing cleanup is the call to term_provide_backend(), because that's a terminal API function which is already called to notify the terminal that one backend has gone away and the next one has turned up. (In fact, it's called twice, once to set term->backend to NULL when the first session closes, and again when the session is restarted. I see no harm in making the cleanup unconditional, not bothering to tell the difference between the two cases.) --- terminal/terminal.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/terminal/terminal.c b/terminal/terminal.c index e127ff6e..2db81c9a 100644 --- a/terminal/terminal.c +++ b/terminal/terminal.c @@ -2374,6 +2374,8 @@ void term_resize_request_completed(Terminal *term) void term_provide_backend(Terminal *term, Backend *backend) { term->backend = backend; + if (term->userpass_state) + term_userpass_state_free(term->userpass_state); if (term->backend && term->cols > 0 && term->rows > 0) backend_size(term->backend, term->cols, term->rows); } From 1ce8ec9c825dd3432a515c25b7e01b808b2ad004 Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Sun, 8 Dec 2024 22:08:30 +0000 Subject: [PATCH 24/44] lineedit_send_line: batch up output characters. When the user pressed Return at the end of a line, we were calling the TermLineEditor's receiver function once for each character in the line buffer. A Telnet user reported from looking at packet traces that this leads to each character being sent in its own TCP segment, which is wasteful and silly, and a regression in 0.82 compared to 0.81. You can see the SSH version of the phenomenon even more easily in PuTTY's own SSH logs, without having to look at the TCP layer at all: you get a separate SSH2_MSG_CHANNEL_DATA per character when sending a line that you entered via local editing in the GUI terminal. The fix in this commit makes lineedit_send_line() collect keystrokes into a temporary bufchain and pass them on to the backend in chunks the size of a bufchain block. This is better, but still not completely ideal: lineedit_send_line() is often followed by a call to lineedit_send_newline(), and there's no buffering done between _those_ functions. So you'll still see a separate SSH message / Telnet TCP segment for the newline after the line. I haven't fixed that in this commit, for two reasons. First, unlike the character-by-character sending of the line content, it's not a regression in 0.82: previous versions also sent the newline in a separate packet and nobody complained about that. Second, it's much more difficult, because newlines are handled specially - in particular by the Telnet backend, which sometimes turns them into a wire sequence CR LF that can't be generated by passing any literal byte to backend_send. So you'd need to violate a load of layers, or else have multiple parts of the system buffer up output and then arrange to release it on a toplevel callback or some such. Much more code, more risk of bugs, and less gain. --- terminal/lineedit.c | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/terminal/lineedit.c b/terminal/lineedit.c index eaaa3a18..a5af9a07 100644 --- a/terminal/lineedit.c +++ b/terminal/lineedit.c @@ -157,8 +157,19 @@ static void lineedit_delete_line(TermLineEditor *le) void lineedit_send_line(TermLineEditor *le) { + bufchain output; + bufchain_init(&output); + for (BufChar *bc = le->head; bc; bc = bc->next) - lineedit_send_data(le, make_ptrlen(bc->wire, bc->nwire)); + bufchain_add(&output, bc->wire, bc->nwire); + + while (bufchain_size(&output) > 0) { + ptrlen data = bufchain_prefix(&output); + lineedit_send_data(le, data); + bufchain_consume(&output, data.len); + } + bufchain_clear(&output); + lineedit_free_buffer(le); le->quote_next_char = false; } From 09095a7d9261676871a881aff7df4b39bd2802b4 Mon Sep 17 00:00:00 2001 From: Ben Harris Date: Sat, 15 Jul 2023 21:05:41 +0100 Subject: [PATCH 25/44] Avoid treating non-X GDK display names as X ones When running on Wayland, gdk_display_get_name() can return things like "wayland-0" rather than valid X display names. PuTTY nonetheless treated them as X display names, meaning that when running under Wayland, pterm would set DISPLAY to "wayland-0" in subprocesses, and PuTTY's X forwarding wouldn't work properly. To fix this, places that call gdk_display_get_name() now only do so on displays for which GDK_IS_X_DISPLAY() is true. As with GDK_IS_X_WINDOW(), this requires some backward-compatibility for GDK versions where everything is implicitly running on X. To make this work usefully, pterm now also won't unset DISPLAY if it can't get an X display name and instead will pass through whatever value of DISPLAY it received. I think that's better behaviour anyway. There are two separate parts of PuTTY that call gdk_display_get_name(). platform_get_x_display() in unix/putty.c is used for X forwarding, while gtk_seat_get_x_display() in unix/window.c is used used for setting DISPLAY and recording in utmp. I've updated both of them. --- unix/gtkcompat.h | 1 + unix/pty.c | 2 -- unix/putty.c | 9 ++++++++- unix/window.c | 11 +++++++---- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/unix/gtkcompat.h b/unix/gtkcompat.h index 2e8e8b88..02768323 100644 --- a/unix/gtkcompat.h +++ b/unix/gtkcompat.h @@ -187,6 +187,7 @@ #endif /* 2.24 */ #if !GTK_CHECK_VERSION(3,0,0) +#define GDK_IS_X11_DISPLAY(display) (1) #define GDK_IS_X11_WINDOW(window) (1) #endif diff --git a/unix/pty.c b/unix/pty.c index c03328f7..2e3ca749 100644 --- a/unix/pty.c +++ b/unix/pty.c @@ -1138,8 +1138,6 @@ Backend *pty_backend_create( char *x_display_env_var = dupprintf("DISPLAY=%s", x_display); putenv(x_display_env_var); /* As above, we don't free this. */ - } else { - unsetenv("DISPLAY"); } } #endif diff --git a/unix/putty.c b/unix/putty.c index a96217e3..1ccc10b6 100644 --- a/unix/putty.c +++ b/unix/putty.c @@ -18,6 +18,10 @@ #include "gtkcompat.h" +#ifndef NOT_X_WINDOWS +#include +#endif + /* * Stubs to avoid pty.c needing to be linked in. */ @@ -62,8 +66,11 @@ const bool dup_check_launchable = true; char *platform_get_x_display(void) { const char *display; +#ifndef NOT_X_WINDOWS /* Try to take account of --display and what have you. */ - if (!(display = gdk_get_display())) + if (!GDK_IS_X11_DISPLAY(gdk_display_get_default()) || + !(display = gdk_get_display())) +#endif /* fall back to traditional method */ display = getenv("DISPLAY"); return dupstr(display); diff --git a/unix/window.c b/unix/window.c index 4a8968d6..97977c3c 100644 --- a/unix/window.c +++ b/unix/window.c @@ -411,8 +411,8 @@ StripCtrlChars *gtk_seat_stripctrl_new( static void gtk_seat_notify_remote_exit(Seat *seat); static void gtk_seat_update_specials_menu(Seat *seat); static void gtk_seat_set_busy_status(Seat *seat, BusyStatus status); -static const char *gtk_seat_get_x_display(Seat *seat); #ifndef NOT_X_WINDOWS +static const char *gtk_seat_get_x_display(Seat *seat); static bool gtk_seat_get_windowid(Seat *seat, long *id); #endif static void gtk_seat_set_trust_status(Seat *seat, bool trusted); @@ -439,10 +439,11 @@ static const SeatVtable gtk_seat_vt = { .prompt_descriptions = gtk_seat_prompt_descriptions, .is_utf8 = gtk_seat_is_utf8, .echoedit_update = nullseat_echoedit_update, - .get_x_display = gtk_seat_get_x_display, #ifdef NOT_X_WINDOWS + .get_x_display = nullseat_get_x_display, .get_windowid = nullseat_get_windowid, #else + .get_x_display = gtk_seat_get_x_display, .get_windowid = gtk_seat_get_windowid, #endif .get_window_pixel_size = gtk_seat_get_window_pixel_size, @@ -4348,12 +4349,14 @@ void modalfatalbox(const char *p, ...) exit(1); } +#ifndef NOT_X_WINDOWS static const char *gtk_seat_get_x_display(Seat *seat) { - return gdk_get_display(); + if (GDK_IS_X11_DISPLAY(gdk_display_get_default())) + return gdk_get_display(); + return NULL; } -#ifndef NOT_X_WINDOWS static bool gtk_seat_get_windowid(Seat *seat, long *id) { GtkFrontend *inst = container_of(seat, GtkFrontend, seat); From 7802932eed5bcf5890ed50718eced0c9fbb424bf Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Sun, 15 Dec 2024 09:59:22 +0000 Subject: [PATCH 26/44] Document how to set GIT_SSH_COMMAND to plink -batch. A user helpfully figured this out for us after the changes to Plink's password prompt handling had disrupted their previous workflow. So it seems worth documenting in case anyone else needs this fix. (I think it is a fix, not a workaround: anyone needing this option now probably _should_ have been doing it all along, because with the old behaviour, Plink would have been sending a password prompt to Git, and maybe even interpreting some of Git's protocol output as a password! -batch would have been a more sensible way to abort the connection even before the changes.) --- doc/plink.but | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/doc/plink.but b/doc/plink.but index 4719f057..d4bb183e 100644 --- a/doc/plink.but +++ b/doc/plink.but @@ -243,6 +243,10 @@ This may help Plink's behaviour when it is used in automated scripts: using \c{-batch}, if something goes wrong at connection time, the batch job will fail rather than hang. +If another program is invoking Plink on your behalf, then you might +need to arrange that that program passes \c{-batch} to Plink. See +\k{plink-git} for an example involving Git. + \S2{plink-option-s} \I{-s-plink}\c{-s}: remote command is SSH subsystem If you specify the \c{-s} option, Plink passes the specified command @@ -395,6 +399,38 @@ particular web area: Any non-interactive command you could usefully run on the server command line, you can run in a batch file using Plink in this way. +\H{plink-git} Using Plink with \i{Git} + +To use Plink for Git operations performed over SSH, you can set the +environment variable \i\c{GIT_SSH_COMMAND} to point to Plink. + +For example, if you've run PuTTY's full Windows installer and it has +installed Plink in the default location, you might do this: + +\c set GIT_SSH_COMMAND="C:\Program Files\PuTTY\plink.exe" + +or if you've put Plink somewhere else then you can do a similar thing +with a different path. + +This environment variable accepts a whole command line, not just an +executable file name. So you can add Plink options to the end of it if +you like. For example, if you're using Git in a batch-mode context, +where your Git jobs are running unattended and nobody is available to +answer interactive prompts, you might also append the \cq{-batch} +option (\k{plink-option-batch}): + +\c set GIT_SSH_COMMAND="C:\Program Files\PuTTY\plink.exe" -batch + +and then if Plink unexpectedly prints a prompt of some kind (for +example, because the SSH server's host key has changed), your batch +job will terminate with an error message, instead of stopping and +waiting for user input that will never arrive. + +(However, you don't \e{always} want to do this with Git. If you're +using Git interactively, you might \e{want} Plink to stop for +interactive prompts \dash for example, to let you enter a password for +the SSH server.) + \H{plink-cvs} Using Plink with \i{CVS} To use Plink with CVS, you need to set the environment variable From c91437bae3a9aef1e235a0dc00669dc91a76c6e3 Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Sun, 15 Dec 2024 10:16:01 +0000 Subject: [PATCH 27/44] Update cmake_minimum_required to avoid warnings on sid. The new (ish) "3.7...3.28" syntax means: cmake will give up with a fatal error if you try to build with a version older than 3.7, but also, it won't turn on any new behaviour introduced after 3.28 (which is the cmake version in Ubuntu 24.04, where I'm currently doing both my development and production builds). Without this, cmake 3.31 (found on Debian sid) will give a warning at configure time: "Compatibility with CMake < 3.10 will be removed from a future version of CMake." I guess the point is that they're planning to make breaking changes that arrange that you _can't_ make the same CMakeLists work with both 3.7 and this potential newer version. So by specifying 3.28 as the "max" version, we avoid those breaking changes affecting us, for the moment. Our "old distro support" policy is currently that we still want to be able to (and indeed I actually test it before each release) build on Debian stretch, which is still in support, albeit a very marginal paid-LTS kind of support. So we do still need to support cmake 3.7. This seems to be a plausible way to get that to carry on working, while not provoking annoying warnings from cmake 3.31, or risking the actual breaking change when it comes, whatever it is. (Fun fact: cmake 3.7 doesn't actually _understand_ this 3.7...3.28 syntax! That syntax itself was introduced in 3.12. But the cmake manual explains that it's harmless to earlier versions, which will interpret the extra dots as separating additional version components, and ignore them. :-) --- CMakeLists.txt | 2 +- doc/CMakeLists.txt | 2 +- test/sclog/CMakeLists.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 314b7f6a..dad26e03 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.7) +cmake_minimum_required(VERSION 3.7...3.28) project(putty LANGUAGES C) set(CMAKE_C_STANDARD 99) diff --git a/doc/CMakeLists.txt b/doc/CMakeLists.txt index ef77eb12..35673996 100644 --- a/doc/CMakeLists.txt +++ b/doc/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.7) +cmake_minimum_required(VERSION 3.7...3.28) project(putty-documentation LANGUAGES) # This build script can be run standalone, or included as a diff --git a/test/sclog/CMakeLists.txt b/test/sclog/CMakeLists.txt index 8a8ac570..9f9f75f9 100644 --- a/test/sclog/CMakeLists.txt +++ b/test/sclog/CMakeLists.txt @@ -2,7 +2,7 @@ # goes with the PuTTY test binary 'testsc'. For build instructions see # the comment at the top of testsc.c. -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.7...3.28) project(sclog LANGUAGES C) From 11c7c7608c5199a914d11f9f5d830e83041946f8 Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Sun, 15 Dec 2024 14:25:23 +0000 Subject: [PATCH 28/44] Remove bit-rotted RDB_DEBUG_PATCH. I was about to try to debug a window resizing issue, and it looked as if this patch had a plausible set of diagnostics already. But in fact when I turned on this #ifdef it failed to compile, so I'm getting rid of it. Perhaps there is a use for having types of diagnostic living permanently in the source code and easy to enable in themed sets, but if so, I think they'd be better compiled in and enabled by an option, than compiled out and enabled by #ifdef. That way they're less likely to rot, and also, you can ask a user to turn one on really easily and get extra logs for whatever is bothering them! --- windows/window.c | 54 ------------------------------------------------ 1 file changed, 54 deletions(-) diff --git a/windows/window.c b/windows/window.c index dbe565c7..e580f653 100644 --- a/windows/window.c +++ b/windows/window.c @@ -1504,11 +1504,6 @@ static void init_fonts(WinGuiSeat *wgs, int pick_width, int pick_height) wgs->font_width = get_font_width(wgs, hdc, &tm); } -#ifdef RDB_DEBUG_PATCH - debug("Primary font H=%d, AW=%d, MW=%d\n", - tm.tmHeight, tm.tmAveCharWidth, tm.tmMaxCharWidth); -#endif - { CHARSETINFO info; DWORD cset = tm.tmCharSet; @@ -1795,10 +1790,6 @@ static void reset_window(WinGuiSeat *wgs, int reinit) int win_width, win_height, resize_action, window_border; RECT cr, wr; -#ifdef RDB_DEBUG_PATCH - debug("reset_window()\n"); -#endif - /* Current window sizes ... */ GetWindowRect(wgs->term_hwnd, &wr); GetClientRect(wgs->term_hwnd, &cr); @@ -1814,9 +1805,6 @@ static void reset_window(WinGuiSeat *wgs, int reinit) /* Are we being forced to reload the fonts ? */ if (reinit>1) { -#ifdef RDB_DEBUG_PATCH - debug("reset_window() -- Forced deinit\n"); -#endif deinit_fonts(wgs); init_fonts(wgs, 0, 0); } @@ -1828,9 +1816,6 @@ static void reset_window(WinGuiSeat *wgs, int reinit) /* Is the window out of position ? */ if (!reinit) { recompute_window_offset(wgs); -#ifdef RDB_DEBUG_PATCH - debug("reset_window() -> Reposition terminal\n"); -#endif } if (IsZoomed(wgs->term_hwnd)) { @@ -1857,10 +1842,6 @@ static void reset_window(WinGuiSeat *wgs, int reinit) wgs->offset_height = (win_height - wgs->font_height*wgs->term->rows) / 2; InvalidateRect(wgs->term_hwnd, NULL, true); -#ifdef RDB_DEBUG_PATCH - debug("reset_window() -> Z font resize to (%d, %d)\n", - wgs->font_width, wgs->font_height); -#endif } } else { if (wgs->font_width * wgs->term->cols != win_width || @@ -1877,9 +1858,6 @@ static void reset_window(WinGuiSeat *wgs, int reinit) wgs->offset_height = (win_height - window_border - wgs->font_height*wgs->term->rows) / 2; InvalidateRect(wgs->term_hwnd, NULL, true); -#ifdef RDB_DEBUG_PATCH - debug("reset_window() -> Zoomed term_size\n"); -#endif } } return; @@ -1921,10 +1899,6 @@ static void reset_window(WinGuiSeat *wgs, int reinit) * so we resize to the default font size. */ if (reinit>0) { -#ifdef RDB_DEBUG_PATCH - debug("reset_window() -> Forced re-init\n"); -#endif - wgs->offset_width = wgs->offset_height = window_border; wgs->extra_width = wr.right - wr.left - cr.right + cr.left + wgs->offset_width*2; @@ -2001,10 +1975,6 @@ static void reset_window(WinGuiSeat *wgs, int reinit) if ( width > wgs->term->cols ) width = wgs->term->cols; term_size(wgs->term, height, width, conf_get_int(wgs->conf, CONF_savelines)); -#ifdef RDB_DEBUG_PATCH - debug("reset_window() -> term resize to (%d,%d)\n", - height, width); -#endif } } @@ -2014,11 +1984,6 @@ static void reset_window(WinGuiSeat *wgs, int reinit) SWP_NOMOVE | SWP_NOZORDER); InvalidateRect(wgs->term_hwnd, NULL, true); -#ifdef RDB_DEBUG_PATCH - debug("reset_window() -> window resize to (%d,%d)\n", - wgs->font_width*term->cols + wgs->extra_width, - wgs->font_height*term->rows + wgs->extra_height); -#endif } return; } @@ -2040,10 +2005,6 @@ static void reset_window(WinGuiSeat *wgs, int reinit) wr.bottom - wr.top - cr.bottom + cr.top + wgs->offset_height*2; InvalidateRect(wgs->term_hwnd, NULL, true); -#ifdef RDB_DEBUG_PATCH - debug("reset_window() -> font resize to (%d,%d)\n", - wgs->font_width, wgs->font_height); -#endif } } @@ -2938,9 +2899,6 @@ static LRESULT CALLBACK WndProc(HWND hwnd, UINT message, term_update(wgs->term); break; case WM_ENTERSIZEMOVE: -#ifdef RDB_DEBUG_PATCH - debug("WM_ENTERSIZEMOVE\n"); -#endif EnableSizeTip(true); wgs->resizing = true; wgs->need_backend_resize = false; @@ -2948,9 +2906,6 @@ static LRESULT CALLBACK WndProc(HWND hwnd, UINT message, case WM_EXITSIZEMOVE: EnableSizeTip(false); wgs->resizing = false; -#ifdef RDB_DEBUG_PATCH - debug("WM_EXITSIZEMOVE\n"); -#endif if (wgs->need_backend_resize) { term_size(wgs->term, conf_get_int(wgs->conf, CONF_height), conf_get_int(wgs->conf, CONF_width), @@ -3063,15 +3018,6 @@ static LRESULT CALLBACK WndProc(HWND hwnd, UINT message, break; case WM_SIZE: resize_action = conf_get_int(wgs->conf, CONF_resize_action); -#ifdef RDB_DEBUG_PATCH - debug("WM_SIZE %s (%d,%d)\n", - (wParam == SIZE_MINIMIZED) ? "SIZE_MINIMIZED": - (wParam == SIZE_MAXIMIZED) ? "SIZE_MAXIMIZED": - (wParam == SIZE_RESTORED && resizing) ? "to": - (wParam == SIZE_RESTORED) ? "SIZE_RESTORED": - "...", - LOWORD(lParam), HIWORD(lParam)); -#endif term_notify_minimised(wgs->term, wParam == SIZE_MINIMIZED); { /* From 1fc5f4afd1b554b498f470749237d4fea7a9a405 Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Sun, 15 Dec 2024 14:48:01 +0000 Subject: [PATCH 29/44] wm_size_resize_term: update conf unconditionally. A user reported that when a PuTTY window is resized by the 'FancyZones' tool included in Microsoft PowerToys, the terminal itself knows the new size ('stty' showed that it had sent a correct SIGWINCH to the SSH server), but the next invocation of the Change Settings dialog box still has the old size entered in it, leading to confusing behaviour when you press Apply. Inside PuTTY, this must mean that we updated the actual terminal's size, but didn't update the main Conf object to match it, which is where Change Settings populates its initial dialog state from. It looks as if this is because FancyZones resizes the window by sending it one single WM_SIZE, without wrapping it in the WM_ENTERSIZEMOVE and WM_EXITSIZEMOVE messages that signal the start and end of an interactive dragging resize operation. And the update of Conf in wm_size_resize_term was in only one branch of the if statement that checks whether we're in an interactive resize. Now it's outside the if, so Conf will be updated in both cases. --- windows/window.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/windows/window.c b/windows/window.c index e580f653..b2ac0adb 100644 --- a/windows/window.c +++ b/windows/window.c @@ -2185,12 +2185,12 @@ static void wm_size_resize_term(WinGuiSeat *wgs, LPARAM lParam) * numbers of resize events. */ wgs->need_backend_resize = true; - conf_set_int(wgs->conf, CONF_height, h); - conf_set_int(wgs->conf, CONF_width, w); } else { term_size(wgs->term, h, w, conf_get_int(wgs->conf, CONF_savelines)); } + conf_set_int(wgs->conf, CONF_height, h); + conf_set_int(wgs->conf, CONF_width, w); } static LRESULT CALLBACK WndProc(HWND hwnd, UINT message, From 363debc7f09791a905b94dd6cae96328210ca742 Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Sun, 15 Dec 2024 19:09:34 +0000 Subject: [PATCH 30/44] lineedit: make both ^M and ^J terminate a line. In protocols other than PROT_RAW, the new line editing system differed from the old one in not considering ^M or ^J (typed using the actual Ctrl key, so distinct from pressing Return) to mean "I've finished editing this line, please send it". This commit reinstates that behaviour. It turned out that a third-party tool (namely PuTTY Connection Manager), which automatically answers prompts for the user, was terminating them by sending ^J in place of the Return key. We don't know why (and it's now unmaintained), but it was. So this change should make that tool start working again. I exclude PROT_RAW above because in that protocol the line editing has much weirder handling for ^M and ^J, which lineedit replicated faithfully from the old code: either control character by itself is treated literally (displaying as "^M" or "^J" in the terminal), but if you type the two in sequence in that order, then the ^J deletes the ^M from the edit buffer and enters the line, so that the sequence CR LF acts as a newline overall. I haven't changed that behaviour here, but I have added a regression test of it to test_lineedit. --- terminal/lineedit.c | 18 ++++++++++++++ test/test_lineedit.c | 59 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/terminal/lineedit.c b/terminal/lineedit.c index a5af9a07..807c24db 100644 --- a/terminal/lineedit.c +++ b/terminal/lineedit.c @@ -454,6 +454,24 @@ void lineedit_input(TermLineEditor *le, char ch, bool dedicated) lineedit_complete_line(le); return; } + } else { + /* If we're not in LE_CRLF_NEWLINE mode, then ^J by + * itself acts as a full newline character */ + lineedit_complete_line(le); + return; + } + + + case CTRL('M'): + if (le->flags & LE_CRLF_NEWLINE) { + /* In this mode, ^M is literal, and can combine with + * ^J (see case above). So do nothing, and fall + * through into the 'treat it literally' code, */ + } else { + /* If we're not in LE_CRLF_NEWLINE mode, then ^M by + * itself acts as a full newline character */ + lineedit_complete_line(le); + return; } } } diff --git a/test/test_lineedit.c b/test/test_lineedit.c index fe0be162..3ad037fb 100644 --- a/test/test_lineedit.c +++ b/test/test_lineedit.c @@ -527,6 +527,65 @@ static void test_edit(Mock *mk, bool echo) EXPECT(mk, specials, 1, SS_EOF, 0); reset(mk); + /* ^M with the special flag is the Return key, and sends the line */ + ldisc_send(mk->ldisc, "abc", 3, false); + EXPECT(mk, backend, PTRLEN_LITERAL("")); + EXPECT_TERMINAL(mk, PTRLEN_LITERAL("abc")); + ldisc_send(mk->ldisc, "\x0D", -1, true); + EXPECT(mk, backend, PTRLEN_LITERAL("abc\x0D\x0A")); + EXPECT_TERMINAL(mk, PTRLEN_LITERAL("abc\x0D\x0A")); + reset(mk); + + /* In non-LE_CRLF_NEWLINE mode, either of ^M or ^J without the + * special flag also sends the line */ + conf_set_int(mk->conf, CONF_protocol, PROT_SSH); + ldisc_configure(mk->ldisc, mk->conf); + ldisc_send(mk->ldisc, "abc", 3, false); + EXPECT(mk, backend, PTRLEN_LITERAL("")); + EXPECT_TERMINAL(mk, PTRLEN_LITERAL("abc")); + ldisc_send(mk->ldisc, "\x0D", 1, true); + EXPECT(mk, backend, PTRLEN_LITERAL("abc\x0D")); + EXPECT_TERMINAL(mk, PTRLEN_LITERAL("abc\x0D\x0A")); + reset(mk); + ldisc_send(mk->ldisc, "abc", 3, false); + EXPECT(mk, backend, PTRLEN_LITERAL("")); + EXPECT_TERMINAL(mk, PTRLEN_LITERAL("abc")); + ldisc_send(mk->ldisc, "\x0A", 1, true); + EXPECT(mk, backend, PTRLEN_LITERAL("abc\x0D")); + EXPECT_TERMINAL(mk, PTRLEN_LITERAL("abc\x0D\x0A")); + reset(mk); + + /* In LE_CRLF_NEWLINE mode, non-special ^J is just literal */ + conf_set_int(mk->conf, CONF_protocol, PROT_RAW); + ldisc_configure(mk->ldisc, mk->conf); + ldisc_send(mk->ldisc, "abc", 3, false); + EXPECT(mk, backend, PTRLEN_LITERAL("")); + EXPECT_TERMINAL(mk, PTRLEN_LITERAL("abc")); + ldisc_send(mk->ldisc, "\x0A", 1, true); + EXPECT(mk, backend, PTRLEN_LITERAL("")); + EXPECT_TERMINAL(mk, PTRLEN_LITERAL("abc^J")); + /* So when we press Return it's sent */ + ldisc_send(mk->ldisc, "\x0D", -1, true); + EXPECT(mk, backend, PTRLEN_LITERAL("abc\x0A\x0D\x0A")); + EXPECT_TERMINAL(mk, PTRLEN_LITERAL("abc^J\x0D\x0A")); + reset(mk); + + /* In LE_CRLF_NEWLINE mode, non-special ^M is literal, but if + * followed with ^J, they combine into a Return */ + conf_set_int(mk->conf, CONF_protocol, PROT_RAW); + ldisc_configure(mk->ldisc, mk->conf); + ldisc_send(mk->ldisc, "abc", 3, false); + EXPECT(mk, backend, PTRLEN_LITERAL("")); + EXPECT_TERMINAL(mk, PTRLEN_LITERAL("abc")); + ldisc_send(mk->ldisc, "\x0D", 1, true); + EXPECT(mk, backend, PTRLEN_LITERAL("")); + EXPECT_TERMINAL(mk, PTRLEN_LITERAL("abc^M")); + /* So when we press Return it's sent */ + ldisc_send(mk->ldisc, "\x0A", 1, true); + EXPECT(mk, backend, PTRLEN_LITERAL("abc\x0D\x0A")); + EXPECT_TERMINAL(mk, PTRLEN_LITERAL("abc^M\x08 \x08\x08 \x08\x0D\x0A")); + reset(mk); + /* ^R redraws the current line, after printing "^R" at the end of * the previous attempt to make it clear that that's what * happened */ From 27550b02e26c71a9638ff25aeaeff32183ca5a3f Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Mon, 16 Dec 2024 18:44:47 +0000 Subject: [PATCH 31/44] Windows: inhibit all default application manifests. In 0.81 and before, we put an application manifest (XML-formatted Windows resource) into all the GUI tools on purpose, and the CLI tools like Plink didn't have one. But in 0.82, the CLI tools do have one, and it's a small default one we didn't write ourselves, inserted by some combination of cmake and clang-imitating-MSVC (I haven't checked which of those is the cause). This appears to have happened as a side effect of a build-tools update, not on purpose. And its effect is that Windows XP now objects to our plink.exe, because it's very picky about manifest format (we have an old 'xp-wont-run' bug record about that). Since it seemed to work fine to not have a manifest at all in 0.81, let's go back to that. We were already passing /manifest:no to inhibit the default manifest in the GUI tools, to stop it fighting with our custom one; now I've moved /manifest:no into the global linker flags, so it's applied to _all_ binaries, whether we're putting our own manifest in or not. --- cmake/platforms/windows.cmake | 4 +--- windows/CMakeLists.txt | 20 +++++--------------- 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/cmake/platforms/windows.cmake b/cmake/platforms/windows.cmake index a7ed7c7c..481809ec 100644 --- a/cmake/platforms/windows.cmake +++ b/cmake/platforms/windows.cmake @@ -101,14 +101,12 @@ endif() if(WINELIB) enable_language(RC) - set(LFLAG_MANIFEST_NO "") elseif(CMAKE_C_COMPILER_ID MATCHES "MSVC" OR CMAKE_C_COMPILER_FRONTEND_VARIANT MATCHES "MSVC") set(CMAKE_RC_FLAGS "${CMAKE_RC_FLAGS} /nologo /C1252") - set(LFLAG_MANIFEST_NO "/manifest:no") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /manifest:no") else() set(CMAKE_RC_FLAGS "${CMAKE_RC_FLAGS} -c1252") - set(LFLAG_MANIFEST_NO "") endif() if(STRICT) diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt index 7e16b0f4..aefcb9d2 100644 --- a/windows/CMakeLists.txt +++ b/windows/CMakeLists.txt @@ -83,9 +83,7 @@ add_dependencies(pageant generated_licence_h) target_link_libraries(pageant guimisc eventloop agent network crypto utils ${platform_libraries}) -set_target_properties(pageant PROPERTIES - WIN32_EXECUTABLE ON - LINK_FLAGS "${LFLAG_MANIFEST_NO}") +set_target_properties(pageant PROPERTIES WIN32_EXECUTABLE ON) installed_program(pageant) add_sources_from_current_dir(plink no-jump-list.c nohelp.c plink.rc) @@ -111,9 +109,7 @@ target_link_libraries(putty guiterminal guimisc eventloop sshclient otherbackends settings network crypto utils ${platform_libraries}) -set_target_properties(putty PROPERTIES - WIN32_EXECUTABLE ON - LINK_FLAGS "${LFLAG_MANIFEST_NO}") +set_target_properties(putty PROPERTIES WIN32_EXECUTABLE ON) installed_program(putty) add_executable(puttytel @@ -132,9 +128,7 @@ add_dependencies(puttytel generated_licence_h) target_link_libraries(puttytel guiterminal guimisc eventloop otherbackends settings network utils ${platform_libraries}) -set_target_properties(puttytel PROPERTIES - WIN32_EXECUTABLE ON - LINK_FLAGS "${LFLAG_MANIFEST_NO}") +set_target_properties(puttytel PROPERTIES WIN32_EXECUTABLE ON) installed_program(puttytel) add_executable(puttygen @@ -152,9 +146,7 @@ add_dependencies(puttygen generated_licence_h) target_link_libraries(puttygen keygen guimisc crypto utils ${platform_libraries}) -set_target_properties(puttygen PROPERTIES - WIN32_EXECUTABLE ON - LINK_FLAGS "${LFLAG_MANIFEST_NO}") +set_target_properties(puttygen PROPERTIES WIN32_EXECUTABLE ON) installed_program(puttygen) if(HAVE_CONPTY) @@ -174,9 +166,7 @@ if(HAVE_CONPTY) target_link_libraries(pterm guiterminal guimisc eventloop settings network utils ${platform_libraries}) - set_target_properties(pterm PROPERTIES - WIN32_EXECUTABLE ON - LINK_FLAGS "${LFLAG_MANIFEST_NO}") + set_target_properties(pterm PROPERTIES WIN32_EXECUTABLE ON) installed_program(pterm) else() message("ConPTY not available; cannot build Windows pterm") From c2077f888c4076c7a872477dbfe4dafcc92d978a Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Thu, 19 Dec 2024 08:30:07 +0000 Subject: [PATCH 32/44] Fix compile warnings in tree234 tests. I'm not sure why these have never bothered me before, but a test build I just made for a completely different reason complained about them! findtest() did a binary search using a while loop, and then used variables set in the loop body, which gcc objected to on the grounds that the body might have run 0 times and not initialised those variables. Also in the same function gcc objected to the idea that findrelpos234() might have returned NULL and not set 'index'. I think neither of these things can actually have _happened_, but let's stop the compiler complaining anyway. --- utils/tree234.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/utils/tree234.c b/utils/tree234.c index 463f218c..004cfb8d 100644 --- a/utils/tree234.c +++ b/utils/tree234.c @@ -1398,7 +1398,8 @@ void findtest(void) lo = 0; hi = arraylen - 1; - while (lo <= hi) { + assert(lo <= hi); + do { mid = (lo + hi) / 2; c = strcmp(p, array[mid]); if (c < 0) @@ -1407,7 +1408,7 @@ void findtest(void) lo = mid + 1; else break; - } + } while (lo <= hi); if (c == 0) { if (rel == REL234_LT) @@ -1428,10 +1429,11 @@ void findtest(void) ret = NULL; } + index = -1; realret = findrelpos234(tree, p, NULL, rel, &index); if (realret != ret) { error("find(\"%s\",%s) gave %s should be %s", - p, relnames[j], realret, ret); + p, relnames[j], realret ? realret : "NULL", ret); } if (realret && index != mid) { error("find(\"%s\",%s) gave %d should be %d", From 98200d1bfec13397cf81b1d5b28a1ab90962dcde Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Thu, 19 Dec 2024 08:47:08 +0000 Subject: [PATCH 33/44] Arm: turn on PSTATE.DIT if available and needed. DIT, for 'Data-Independent Timing', is a bit you can set in the processor state on sufficiently new Arm CPUs, which promises that a long list of instructions will deliberately avoid varying their timing based on the input register values. Just what you want for keeping your constant-time crypto primitives constant-time. As far as I'm aware, no CPU has _yet_ implemented any data-dependent optimisations, so DIT is a safety precaution against them doing so in future. It would be embarrassing to be caught without it if a future CPU does do that, so we now turn on DIT in the PuTTY process state. I've put a call to the new enable_dit() function at the start of every main() and WinMain() belonging to a program that might do cryptography (even testcrypt, in case someone uses it for something!), and in case I missed one there, also added a second call at the first moment that any cryptography-using part of the code looks as if it might become active: when an instance of the SSH protocol object is configured, when the system PRNG is initialised, and when selecting any cryptographic authentication protocol in an HTTP or SOCKS proxy connection. With any luck those precautions between them should ensure it's on whenever we need it. Arm's own recommendation is that you should carefully choose the granularity at which you enable and disable DIT: there's a potential time cost to turning it on and off (I'm not sure what, but plausibly something of the order of a pipeline flush), so it's a performance hit to do it _inside_ each individual crypto function, but if CPUs start supporting significant data-dependent optimisation in future, then it will also become a noticeable performance hit to just leave it on across the whole process. So you'd like to do it somewhere in the middle: for example, you might turn on DIT once around the whole process of verifying and decrypting an SSH packet, instead of once for decryption and once for MAC. With all respect to that recommendation as a strategy for maximum performance, I'm not following it here. I turn on DIT at the start of the PuTTY process, and then leave it on. Rationale: 1. PuTTY is not otherwise a performance-critical application: it's not likely to max out your CPU for any purpose _other_ than cryptography. The most CPU-intensive non-cryptographic thing I can imagine a PuTTY process doing is the complicated computation of font rendering in the terminal, and that will normally be cached (you don't recompute each glyph from its outline and hints for every time you display it). 2. I think a bigger risk lies in accidental side channels from having DIT turned off when it should have been on. I can imagine lots of causes for that. Missing a crypto operation in some unswept corner of the code; confusing control flow (like my coroutine macros) jumping with DIT clear into the middle of a region of code that expected DIT to have been set at the beginning; having a reference counter of DIT requests and getting it out of sync. In a more sophisticated programming language, it might be possible to avoid the risk in #2 by cleverness with the type system. For example, in Rust, you could have a zero-sized type that acts as a proof token for DIT being enabled (it would be constructed by a function that also sets DIT, have a Drop implementation that clears DIT, and be !Send so you couldn't use it in a thread other than the one where DIT was set), and then you could require all the actual crypto functions to take a DitToken as an extra parameter, at zero runtime cost. Then "oops I forgot to set DIT around this piece of crypto" would become a compile error. Even so, you'd have to take some care with coroutine-structured code (what happens if a Rust async function yields while holding a DIT token?) and with nesting (if you have two DIT tokens, you don't want dropping the inner one to clear DIT while the outer one is still there to wrongly convince callees that it's set). Maybe in Rust you could get this all to work reliably. But not in C! DIT is an optional feature of the Arm architecture, so we must first test to see if it's supported. This is done the same way as we already do for the various Arm crypto accelerators: on ELF-based systems, check the appropriate bit in the 'hwcap' words in the ELF aux vector; on Mac, look for an appropriate sysctl flag. On Windows I don't know of a way to query the DIT feature, _or_ of a way to write the necessary enabling instruction in an MSVC-compatible way. I've _heard_ that it might not be necessary, because Windows might just turn on DIT unconditionally and leave it on, in an even more extreme version of my own strategy. I don't have a source for that - I heard it by word of mouth - but I _hope_ it's true, because that would suit me very well! Certainly I can't write code to enable DIT without knowing (a) how to do it, (b) how to know if it's safe. Nonetheless, I've put the enable_dit() call in all the right places in the Windows main programs as well as the Unix and cross-platform code, so that if I later find out that I _can_ put in an explicit enable of DIT in some way, I'll only have to arrange to set HAVE_ARM_DIT and compile the enable_dit() function appropriately. --- cmake/cmake.h.in | 1 + cmdgen.c | 2 ++ crypto/CMakeLists.txt | 7 +++++++ crypto/enable_dit.c | 24 ++++++++++++++++++++++++ proxy/cproxy.c | 3 +++ pscp.c | 1 + psftp.c | 1 + ssh.h | 11 +++++++++++ ssh/ssh.c | 2 ++ sshrand.c | 4 +++- stubs/no-dit.c | 15 +++++++++++++++ test/testcrypt.c | 2 ++ test/testsc.c | 5 +++++ unix/CMakeLists.txt | 3 +++ unix/askpass.c | 2 ++ unix/main-gtk-application.c | 3 +++ unix/main-gtk-simple.c | 2 ++ unix/pageant.c | 2 ++ unix/plink.c | 2 ++ unix/psusan.c | 2 ++ unix/putty.c | 1 + unix/sftp.c | 1 + unix/uppity.c | 2 ++ unix/utils/arm_arch_queries.c | 15 +++++++++++++++ windows/CMakeLists.txt | 2 ++ windows/pageant.c | 1 + windows/plink.c | 1 + windows/puttygen.c | 1 + windows/sftp.c | 1 + windows/utils/arm_arch_queries.c | 8 ++++++++ windows/window.c | 1 + 31 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 crypto/enable_dit.c create mode 100644 stubs/no-dit.c diff --git a/cmake/cmake.h.in b/cmake/cmake.h.in index f8fcbc58..3d36b3d0 100644 --- a/cmake/cmake.h.in +++ b/cmake/cmake.h.in @@ -60,3 +60,4 @@ #cmakedefine01 HAVE_NEON_SHA512 #cmakedefine01 HAVE_NEON_SHA512_INTRINSICS #cmakedefine01 USE_ARM64_NEON_H +#cmakedefine01 HAVE_ARM_DIT diff --git a/cmdgen.c b/cmdgen.c index 295cdd77..9883fd26 100644 --- a/cmdgen.c +++ b/cmdgen.c @@ -264,6 +264,8 @@ int main(int argc, char **argv) ppk_save_parameters params = ppk_save_default_parameters; FingerprintType fptype = SSH_FPTYPE_DEFAULT; + enable_dit(); + if (is_interactive()) progress_fp = stderr; diff --git a/crypto/CMakeLists.txt b/crypto/CMakeLists.txt index 6358569a..e57bb600 100644 --- a/crypto/CMakeLists.txt +++ b/crypto/CMakeLists.txt @@ -236,6 +236,12 @@ if(neon) endif() endif() +test_compile_with_flags(HAVE_ARM_DIT + GNU_FLAGS -march=armv8.4-a + TEST_SOURCE " + int main(void) { asm volatile(\"msr dit, %0\" :: \"r\"(1)); }" + ADD_SOURCES_IF_SUCCESSFUL enable_dit.c) + set(HAVE_AES_NI ${HAVE_AES_NI} PARENT_SCOPE) set(HAVE_SHA_NI ${HAVE_SHA_NI} PARENT_SCOPE) set(HAVE_SHAINTRIN_H ${HAVE_SHAINTRIN_H} PARENT_SCOPE) @@ -243,3 +249,4 @@ set(HAVE_NEON_CRYPTO ${HAVE_NEON_CRYPTO} PARENT_SCOPE) set(HAVE_NEON_SHA512 ${HAVE_NEON_SHA512} PARENT_SCOPE) set(HAVE_NEON_SHA512_INTRINSICS ${HAVE_NEON_SHA512_INTRINSICS} PARENT_SCOPE) set(USE_ARM64_NEON_H ${USE_ARM64_NEON_H} PARENT_SCOPE) +set(HAVE_ARM_DIT ${HAVE_ARM_DIT} PARENT_SCOPE) diff --git a/crypto/enable_dit.c b/crypto/enable_dit.c new file mode 100644 index 00000000..7c9ec4b4 --- /dev/null +++ b/crypto/enable_dit.c @@ -0,0 +1,24 @@ +/* + * Enable the PSTATE.DIT flag in AArch64, if available. + * + * This guarantees that data-processing instructions (or rather, a + * long list of specific ones) will have data-independent timing + * (hence the name). In other words, you want to turn this bit on if + * you're trying to do constant-time crypto. + * + * For maximum performance you'd want to turn this bit back off when + * doing any CPU-intensive stuff that _isn't_ cryptographic. That + * seems like a small concern in this code base, and carries the risk + * of losing track of whether it was on or not, so here we just enable + * it for the whole process. That's why there's only an enable_dit() + * function in this file and not a disable_dit() to go with it. + */ + +#include "ssh.h" + +void enable_dit(void) +{ + if (!platform_dit_available()) + return; + asm volatile("msr dit, %0" :: "r"(1)); +} diff --git a/proxy/cproxy.c b/proxy/cproxy.c index 40a2f609..4203eb5d 100644 --- a/proxy/cproxy.c +++ b/proxy/cproxy.c @@ -22,6 +22,7 @@ strbuf *chap_response(ptrlen challenge, ptrlen password) { strbuf *sb = strbuf_new_nm(); const ssh2_macalg *alg = &ssh_hmac_md5; + enable_dit(); /* just in case main() forgot */ mac_simple(alg, password, challenge, strbuf_append(sb, alg->len)); return sb; } @@ -75,6 +76,8 @@ void http_digest_response(BinarySink *bs, ptrlen username, ptrlen password, const ssh_hashalg *alg = httphashalgs[hash]; size_t hashlen = httphashlengths[hash]; + enable_dit(); /* just in case main() forgot */ + unsigned char ncbuf[4]; PUT_32BIT_MSB_FIRST(ncbuf, nonce_count); diff --git a/pscp.c b/pscp.c index e3bbf1f3..c7c4623f 100644 --- a/pscp.c +++ b/pscp.c @@ -2274,6 +2274,7 @@ int psftp_main(CmdlineArgList *arglist) bool sanitise_stderr = true; sk_init(); + enable_dit(); /* Load Default Settings before doing anything else. */ conf = conf_new(); diff --git a/psftp.c b/psftp.c index 69044694..b33e29a9 100644 --- a/psftp.c +++ b/psftp.c @@ -2803,6 +2803,7 @@ int psftp_main(CmdlineArgList *arglist) Filename *batchfile = NULL; sk_init(); + enable_dit(); userhost = user = NULL; diff --git a/ssh.h b/ssh.h index 0dd3e6c0..0d31a4bc 100644 --- a/ssh.h +++ b/ssh.h @@ -1312,6 +1312,7 @@ bool platform_pmull_neon_available(void); bool platform_sha256_neon_available(void); bool platform_sha1_neon_available(void); bool platform_sha512_neon_available(void); +bool platform_dit_available(void); /* * PuTTY version number formatted as an SSH version string. @@ -2048,3 +2049,13 @@ enum { PLUGIN_NOTYPE = 256, /* packet too short to have a type */ PLUGIN_EOF = 257 /* EOF from auth plugin */ }; + +/* + * CPU features for security + */ + +#if HAVE_ARM_DIT +void enable_dit(void); +#else +#define enable_dit() ((void)0) +#endif diff --git a/ssh/ssh.c b/ssh/ssh.c index 87b1a039..6d72ed1c 100644 --- a/ssh/ssh.c +++ b/ssh/ssh.c @@ -954,6 +954,8 @@ static char *ssh_init(const BackendVtable *vt, Seat *seat, { Ssh *ssh; + enable_dit(); /* just in case main() forgot */ + ssh = snew(Ssh); memset(ssh, 0, sizeof(Ssh)); diff --git a/sshrand.c b/sshrand.c index 7f3d6411..18720adc 100644 --- a/sshrand.c +++ b/sshrand.c @@ -93,8 +93,10 @@ void random_save_seed(void) void random_ref(void) { - if (!random_active++) + if (!random_active++) { + enable_dit(); /* just in case main() forgot */ random_create(&ssh_sha256); + } } void random_setup_custom(const ssh_hashalg *hash) diff --git a/stubs/no-dit.c b/stubs/no-dit.c new file mode 100644 index 00000000..8ba3b23d --- /dev/null +++ b/stubs/no-dit.c @@ -0,0 +1,15 @@ +/* + * Stub version of enable_dit(), included in applications like + * PuTTYtel and pterm which completely leave out the 'crypto' source + * directory. + */ + +#include "ssh.h" + +#if HAVE_ARM_DIT + +void enable_dit(void) +{ +} + +#endif diff --git a/test/testcrypt.c b/test/testcrypt.c index 328ffc47..fa3df196 100644 --- a/test/testcrypt.c +++ b/test/testcrypt.c @@ -1684,6 +1684,8 @@ int main(int argc, char **argv) const char *infile = NULL, *outfile = NULL; bool doing_opts = true; + enable_dit(); /* in case this is used as a crypto helper (Hyrum's Law) */ + while (--argc > 0) { char *p = *++argv; diff --git a/test/testsc.c b/test/testsc.c index b4950d8d..d38dd639 100644 --- a/test/testsc.c +++ b/test/testsc.c @@ -1879,6 +1879,11 @@ int main(int argc, char **argv) bool keep_outfiles = false; bool test_names_given = false; + /* One day, perhaps, if I ever get this test to work on Arm, we + * might actually _check_ DIT is enabled, and check we're sticking + * to the precise list of DIT-affected instructions */ + enable_dit(); + memset(tests_to_run, 1, sizeof(tests_to_run)); random_hash = ssh_hash_new(&ssh_sha256); diff --git a/unix/CMakeLists.txt b/unix/CMakeLists.txt index 4d8ef964..87ee6574 100644 --- a/unix/CMakeLists.txt +++ b/unix/CMakeLists.txt @@ -147,6 +147,7 @@ if(GTK_FOUND) ${CMAKE_SOURCE_DIR}/stubs/no-gss.c ${CMAKE_SOURCE_DIR}/stubs/no-ca-config.c ${CMAKE_SOURCE_DIR}/stubs/no-console.c + ${CMAKE_SOURCE_DIR}/stubs/no-dit.c ${CMAKE_SOURCE_DIR}/proxy/nosshproxy.c pty.c) be_list(pterm pterm) @@ -163,6 +164,7 @@ if(GTK_FOUND) ${CMAKE_SOURCE_DIR}/stubs/no-gss.c ${CMAKE_SOURCE_DIR}/stubs/no-ca-config.c ${CMAKE_SOURCE_DIR}/stubs/no-console.c + ${CMAKE_SOURCE_DIR}/stubs/no-dit.c ${CMAKE_SOURCE_DIR}/proxy/nosshproxy.c pty.c) be_list(ptermapp pterm) @@ -204,6 +206,7 @@ if(GTK_FOUND) ${CMAKE_SOURCE_DIR}/stubs/no-ca-config.c ${CMAKE_SOURCE_DIR}/stubs/no-console.c ${CMAKE_SOURCE_DIR}/stubs/no-rand.c + ${CMAKE_SOURCE_DIR}/stubs/no-dit.c ${CMAKE_SOURCE_DIR}/proxy/nocproxy.c ${CMAKE_SOURCE_DIR}/proxy/nosshproxy.c) be_list(puttytel PuTTYtel SERIAL OTHERBACKENDS) diff --git a/unix/askpass.c b/unix/askpass.c index a1143a8f..bdd62519 100644 --- a/unix/askpass.c +++ b/unix/askpass.c @@ -612,6 +612,8 @@ int main(int argc, char **argv) int exitcode; char *ret; + enable_dit(); /* maybe overkill, but we _are_ handling a secret */ + gtk_init(&argc, &argv); if (argc != 2) { diff --git a/unix/main-gtk-application.c b/unix/main-gtk-application.c index 3aaf6af7..cb9eeb1f 100644 --- a/unix/main-gtk-application.c +++ b/unix/main-gtk-application.c @@ -78,6 +78,7 @@ I suppose I'll have to look into OS X code signing. #define MAY_REFER_TO_GTK_IN_HEADERS #include "putty.h" +#include "ssh.h" #include "gtkmisc.h" #include "gtkcompat.h" @@ -294,6 +295,8 @@ int main(int argc, char **argv) { int status; + enable_dit(); + /* Call the function in ux{putty,pterm}.c to do app-type * specific setup */ setup(false); /* false means we are not a one-session process */ diff --git a/unix/main-gtk-simple.c b/unix/main-gtk-simple.c index a9a897ca..f1e57012 100644 --- a/unix/main-gtk-simple.c +++ b/unix/main-gtk-simple.c @@ -31,6 +31,7 @@ #define MAY_REFER_TO_GTK_IN_HEADERS #include "putty.h" +#include "ssh.h" #include "terminal.h" #include "gtkcompat.h" #include "unifont.h" @@ -594,6 +595,7 @@ int main(int argc, char **argv) bool need_config_box; setlocale(LC_CTYPE, ""); + enable_dit(); /* Call the function in ux{putty,pterm}.c to do app-type * specific setup */ diff --git a/unix/pageant.c b/unix/pageant.c index bd6d6d7f..9ac9a640 100644 --- a/unix/pageant.c +++ b/unix/pageant.c @@ -1313,6 +1313,8 @@ int main(int argc, char **argv) const char *symlink_path = NULL; FILE *logfp = NULL; + enable_dit(); + progname = argv[0]; /* diff --git a/unix/plink.c b/unix/plink.c index b4a9749f..d5db2c2a 100644 --- a/unix/plink.c +++ b/unix/plink.c @@ -683,6 +683,8 @@ int main(int argc, char **argv) struct winsize size; const struct BackendVtable *backvt; + enable_dit(); + /* * Initialise port and protocol to sensible defaults. (These * will be overridden by more or less anything.) diff --git a/unix/psusan.c b/unix/psusan.c index 4e8693af..268dad25 100644 --- a/unix/psusan.c +++ b/unix/psusan.c @@ -348,6 +348,8 @@ int main(int argc, char **argv) Conf *conf = make_ssh_server_conf(); + enable_dit(); + memset(&ssc, 0, sizeof(ssc)); ssc.application_name = "PSUSAN"; diff --git a/unix/putty.c b/unix/putty.c index 1ccc10b6..a1dd78c5 100644 --- a/unix/putty.c +++ b/unix/putty.c @@ -87,6 +87,7 @@ const unsigned cmdline_tooltype = void setup(bool single) { sk_init(); + enable_dit(); settings_set_default_protocol(be_default_protocol); /* Find the appropriate default port. */ { diff --git a/unix/sftp.c b/unix/sftp.c index 9fb0bf6e..f2323ddd 100644 --- a/unix/sftp.c +++ b/unix/sftp.c @@ -576,6 +576,7 @@ const bool buildinfo_gtk_relevant = false; */ int main(int argc, char *argv[]) { + enable_dit(); uxsel_init(); CmdlineArgList *arglist = cmdline_arg_list_from_argv(argc, argv); return psftp_main(arglist); diff --git a/unix/uppity.c b/unix/uppity.c index 30501af7..5832ed98 100644 --- a/unix/uppity.c +++ b/unix/uppity.c @@ -650,6 +650,8 @@ int main(int argc, char **argv) struct cmdline_instance *ci = &instances[ninstances++]; init_cmdline_instance(ci); + enable_dit(); + if (argc <= 1) { /* * We're going to terminate with an error message below, diff --git a/unix/utils/arm_arch_queries.c b/unix/utils/arm_arch_queries.c index c3dc286b..190ef71e 100644 --- a/unix/utils/arm_arch_queries.c +++ b/unix/utils/arm_arch_queries.c @@ -92,6 +92,21 @@ bool platform_sha512_neon_available(void) #endif } +bool platform_dit_available(void) +{ +#if defined HWCAP_DIT + return getauxval(AT_HWCAP) & HWCAP_DIT; +#elif defined HWCAP2_DIT + return getauxval(AT_HWCAP2) & HWCAP2_DIT; +#elif defined __APPLE__ + SysctlResult res = test_sysctl_flag("hw.optional.arm.FEAT_DIT"); + /* As above, treat 'missing' as enabled */ + return res != SYSCTL_OFF; +#else + return false; +#endif +} + #else /* defined __arm__ || defined __aarch64__ */ /* diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt index aefcb9d2..960a440d 100644 --- a/windows/CMakeLists.txt +++ b/windows/CMakeLists.txt @@ -120,6 +120,7 @@ add_executable(puttytel ${CMAKE_SOURCE_DIR}/stubs/no-ca-config.c ${CMAKE_SOURCE_DIR}/stubs/no-console.c ${CMAKE_SOURCE_DIR}/stubs/no-rand.c + ${CMAKE_SOURCE_DIR}/stubs/no-dit.c ${CMAKE_SOURCE_DIR}/proxy/nocproxy.c ${CMAKE_SOURCE_DIR}/proxy/nosshproxy.c puttytel.rc) @@ -159,6 +160,7 @@ if(HAVE_CONPTY) ${CMAKE_SOURCE_DIR}/stubs/no-ca-config.c ${CMAKE_SOURCE_DIR}/stubs/no-console.c ${CMAKE_SOURCE_DIR}/stubs/no-rand.c + ${CMAKE_SOURCE_DIR}/stubs/no-dit.c ${CMAKE_SOURCE_DIR}/proxy/nosshproxy.c pterm.rc) be_list(pterm pterm) diff --git a/windows/pageant.c b/windows/pageant.c index 202a10f8..a519926f 100644 --- a/windows/pageant.c +++ b/windows/pageant.c @@ -1520,6 +1520,7 @@ int WINAPI WinMain(HINSTANCE inst, HINSTANCE prev, LPSTR cmdline, int show) size_t nclkeys = 0, clkeysize = 0; dll_hijacking_protection(); + enable_dit(); hinst = inst; diff --git a/windows/plink.c b/windows/plink.c index 30779cef..face662f 100644 --- a/windows/plink.c +++ b/windows/plink.c @@ -296,6 +296,7 @@ int main(int argc, char **argv) const struct BackendVtable *vt; dll_hijacking_protection(); + enable_dit(); /* * Initialise port and protocol to sensible defaults. (These diff --git a/windows/puttygen.c b/windows/puttygen.c index c5115c06..8dcf666b 100644 --- a/windows/puttygen.c +++ b/windows/puttygen.c @@ -2370,6 +2370,7 @@ int WINAPI WinMain(HINSTANCE inst, HINSTANCE prev, LPSTR cmdline, int show) struct InitialParams params[1]; dll_hijacking_protection(); + enable_dit(); init_common_controls(); hinst = inst; diff --git a/windows/sftp.c b/windows/sftp.c index fb3658d7..64c1daf6 100644 --- a/windows/sftp.c +++ b/windows/sftp.c @@ -649,6 +649,7 @@ int main(int argc, char *argv[]) int ret; dll_hijacking_protection(); + enable_dit(); CmdlineArgList *arglist = cmdline_arg_list_from_GetCommandLineW(); ret = psftp_main(arglist); diff --git a/windows/utils/arm_arch_queries.c b/windows/utils/arm_arch_queries.c index b0193276..87728e10 100644 --- a/windows/utils/arm_arch_queries.c +++ b/windows/utils/arm_arch_queries.c @@ -43,3 +43,11 @@ bool platform_sha512_neon_available(void) * SHA-512 architecture extension. */ return false; } + +bool platform_dit_available(void) +{ + /* As of 2024-12-17, as far as I can tell from docs.microsoft.com, + * Windows on Arm does not yet provide a PF_ARM_V8_* flag for the + * DIT bit in PSTATE. */ + return false; +} diff --git a/windows/window.c b/windows/window.c index b2ac0adb..efc028e9 100644 --- a/windows/window.c +++ b/windows/window.c @@ -448,6 +448,7 @@ int WINAPI WinMain(HINSTANCE inst, HINSTANCE prev, LPSTR cmdline, int show) int guess_width, guess_height; dll_hijacking_protection(); + enable_dit(); hinst = inst; hprev = prev; From 1e4519976164bd5e379db92611a9e2e29e09ec79 Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Thu, 26 Dec 2024 11:40:38 +0000 Subject: [PATCH 34/44] Treat SOS and PM terminal escape sequences like APC This is a cherry-pick of Stefan Tauner's patch from main, but without my followup refactoring, since the refactoring seemed to me to have a (small but easily avoidable) chance of introducing a bug in 0.83. The only downside of the original patch is that it contains a variable name telling a lie: 'osc_is_apc' should really read 'this isn't an OSC but one of APC, SOS and PM'. But we don't actually treat those three things differently, so the functionality is fine. (cherry picked from commit b6b95f23e563211437e51322edc9118b63a3ca40) --- terminal/terminal.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/terminal/terminal.c b/terminal/terminal.c index 2db81c9a..ad309436 100644 --- a/terminal/terminal.c +++ b/terminal/terminal.c @@ -4127,9 +4127,11 @@ static void term_out(Terminal *term, bool called_from_term_data) term->esc_args[0] = 0; term->esc_nargs = 1; break; + case 'X': /* SOS: Start of String */ + case '^': /* PM: privacy message */ case '_': /* APC: application program command */ - /* APC sequences are just a string, terminated by - * ST or (I've observed in practice) ^G. That is, + /* SOS, PM, and APC sequences are just a string, terminated by + * ST or (I've observed in practice for APC) ^G. That is, * they have the same termination convention as * OSC. So we handle them by going straight into * OSC_STRING state and setting a flag indicating From 457eb6127f1849bee503ae7edf67ed6cafc3b2ac Mon Sep 17 00:00:00 2001 From: Jacob Nevins Date: Tue, 7 Jan 2025 21:04:43 +0000 Subject: [PATCH 35/44] It's a new year. (cherry picked from commit e3272f19e0f3340e854c8aaaf0351a901de0e7be) --- LICENCE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENCE b/LICENCE index fda9c062..09155657 100644 --- a/LICENCE +++ b/LICENCE @@ -1,4 +1,4 @@ -PuTTY is copyright 1997-2024 Simon Tatham. +PuTTY is copyright 1997-2025 Simon Tatham. Portions copyright Robert de Bath, Joris van Rantwijk, Delian Delchev, Andreas Schultz, Jeroen Massar, Wez Furlong, Nicolas Barry, From f58ddf26fe4edbd5793815cbc43f5b91f7a11e69 Mon Sep 17 00:00:00 2001 From: Jacob Nevins Date: Tue, 7 Jan 2025 21:04:54 +0000 Subject: [PATCH 36/44] Windows: fix leak of a Filename. Introduced in f8e1a2b3a9. (cherry picked from commit 6ec424059cf3a140ea5f128cb858cd566551aa96) --- windows/controls.c | 1 + 1 file changed, 1 insertion(+) diff --git a/windows/controls.c b/windows/controls.c index e1fcc589..b6f559c5 100644 --- a/windows/controls.c +++ b/windows/controls.c @@ -2014,6 +2014,7 @@ bool winctrl_handle_command(struct dlgparam *dp, UINT msg, ctrl->handler(ctrl, dp, dp->data, EVENT_ACTION); c->data = NULL; } + filename_free(fn); } } break; From 293be04298e91b0b0a86230df60e72346da9987f Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Mon, 13 Jan 2025 20:47:01 +0000 Subject: [PATCH 37/44] GTK: cherry-pick font defaults and fallbacks from main. This is a combined cherry-pick of three consecutive commits from main: b088d77d580b8f7 GTK: hard-code some last-ditch fallback fonts. 7f4cccde2ae53c0 GTK: fixes to the previous font fallback patch. 6155365076c47a8 GTK: switch the default to client-side fonts. The combined effect is that now PuTTY's built-in default font is client-side rather than server-side (advantaging Wayland and disadvantaging legacy GTK1 builds, which seems like a sensible tradeoff these days), and also, if the configured main font can't be found, we'll try falling back to either the client- or server-side default (whichever is available) before giving up completely and whinging on standard error. --- unix/platform.h | 22 ++++++++++++----- unix/window.c | 65 +++++++++++++++++++++++++++++++++++++------------ 2 files changed, 65 insertions(+), 22 deletions(-) diff --git a/unix/platform.h b/unix/platform.h index 670febd7..fb3629ee 100644 --- a/unix/platform.h +++ b/unix/platform.h @@ -394,13 +394,23 @@ void setup_fd_socket(Socket *s, int infd, int outfd, int inerrfd); void fd_socket_set_psb_prefix(Socket *s, const char *prefix); /* - * Default font setting, which can vary depending on NOT_X_WINDOWS. + * Default font settings. We have a default font for each of + * client-side and server-side, so that we can use one of each as a + * fallback, and we also have a single overall default which goes into + * Conf to populate the initial state of Default Settings. + * + * In the past, this default varied with NOT_X_WINDOWS. But these days + * non-X11 environments like Wayland with only client-side fonts are + * common, and even an X11-capable _build_ of PuTTY is quite likely to + * find out at run time that X11 and its bitmap fonts aren't + * available. Also, a fixed-size bitmap font doesn't play nicely with + * high-DPI displays. And the GTK1 build of PuTTY, which can _only_ + * handle server-side fonts, is legacy. So the default font is + * unconditionally the client-side one. */ -#ifdef NOT_X_WINDOWS -#define DEFAULT_GTK_FONT "client:Monospace 12" -#else -#define DEFAULT_GTK_FONT "server:fixed" -#endif +#define DEFAULT_GTK_CLIENT_FONT "client:Monospace 12" +#define DEFAULT_GTK_SERVER_FONT "server:fixed" +#define DEFAULT_GTK_FONT DEFAULT_GTK_CLIENT_FONT /* * pty.c. diff --git a/unix/window.c b/unix/window.c index 97977c3c..7302dc2c 100644 --- a/unix/window.c +++ b/unix/window.c @@ -4368,22 +4368,22 @@ static bool gtk_seat_get_windowid(Seat *seat, long *id) } #endif -char *setup_fonts_ucs(GtkFrontend *inst) +char *setup_fonts_ucs(GtkFrontend *inst, Conf *conf) { - bool shadowbold = conf_get_bool(inst->conf, CONF_shadowbold); - int shadowboldoffset = conf_get_int(inst->conf, CONF_shadowboldoffset); + bool shadowbold = conf_get_bool(conf, CONF_shadowbold); + int shadowboldoffset = conf_get_int(conf, CONF_shadowboldoffset); FontSpec *fs; unifont *fonts[4]; int i; - fs = conf_get_fontspec(inst->conf, CONF_font); + fs = conf_get_fontspec(conf, CONF_font); fonts[0] = multifont_create(inst->area, fs->name, false, false, shadowboldoffset, shadowbold); if (!fonts[0]) { return dupprintf("unable to load font \"%s\"", fs->name); } - fs = conf_get_fontspec(inst->conf, CONF_boldfont); + fs = conf_get_fontspec(conf, CONF_boldfont); if (shadowbold || !fs->name[0]) { fonts[1] = NULL; } else { @@ -4396,7 +4396,7 @@ char *setup_fonts_ucs(GtkFrontend *inst) } } - fs = conf_get_fontspec(inst->conf, CONF_widefont); + fs = conf_get_fontspec(conf, CONF_widefont); if (fs->name[0]) { fonts[2] = multifont_create(inst->area, fs->name, true, false, shadowboldoffset, shadowbold); @@ -4410,7 +4410,7 @@ char *setup_fonts_ucs(GtkFrontend *inst) fonts[2] = NULL; } - fs = conf_get_fontspec(inst->conf, CONF_wideboldfont); + fs = conf_get_fontspec(conf, CONF_wideboldfont); if (shadowbold || !fs->name[0]) { fonts[3] = NULL; } else { @@ -4861,7 +4861,7 @@ static void after_change_settings_dialog(void *vctx, int retval) conf_get_bool(newconf, CONF_shadowbold) || conf_get_int(oldconf, CONF_shadowboldoffset) != conf_get_int(newconf, CONF_shadowboldoffset)) { - char *errmsg = setup_fonts_ucs(inst); + char *errmsg = setup_fonts_ucs(inst, inst->conf); if (errmsg) { char *msgboxtext = dupprintf("Could not change fonts in terminal window: %s\n", @@ -4950,7 +4950,7 @@ static void change_font_size(GtkFrontend *inst, int increment) } } - errmsg = setup_fonts_ucs(inst); + errmsg = setup_fonts_ucs(inst, inst->conf); if (errmsg) goto cleanup; @@ -5350,15 +5350,48 @@ void new_session_window(Conf *conf, const char *geometry_string) inst->area = gtk_drawing_area_new(); gtk_widget_set_name(GTK_WIDGET(inst->area), "drawing-area"); + /* + * Try to create the fonts for use in the window. If this fails, + * we'll try again with some fallback settings, and only abort + * completely if we can't find any fonts at all. + */ { - char *errmsg = setup_fonts_ucs(inst); - if (errmsg) { - window_setup_error(errmsg); - sfree(errmsg); - gtk_widget_destroy(inst->area); - sfree(inst); - return; + char *errmsg_main = setup_fonts_ucs(inst, inst->conf); + if (!errmsg_main) + goto fonts_ok; + + static const char *const fallbacks[] = { + DEFAULT_GTK_CLIENT_FONT, + DEFAULT_GTK_SERVER_FONT, + }; + for (size_t i = 0; i < lenof(fallbacks); i++) { + Conf *fallback_conf = conf_new(); + do_defaults(NULL, fallback_conf); + + FontSpec *fs = fontspec_new(fallbacks[i]); + conf_set_fontspec(fallback_conf, CONF_font, fs); + fontspec_free(fs); + + char *errmsg_fallback = setup_fonts_ucs(inst, fallback_conf); + conf_free(fallback_conf); + + if (!errmsg_fallback) { + fprintf(stderr, "%s; falling back to default font '%s'\n", + errmsg_main, fallbacks[i]); + sfree(errmsg_main); + goto fonts_ok; + } + + sfree(errmsg_fallback); } + + window_setup_error(errmsg_main); + sfree(errmsg_main); + gtk_widget_destroy(inst->area); + sfree(inst); + return; + + fonts_ok:; } #if GTK_CHECK_VERSION(2,0,0) From e7acb9f6968d48217a4210dd91b742e82f80bc72 Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Mon, 13 Jan 2025 20:43:22 +0000 Subject: [PATCH 38/44] GetDlgItemTextW_alloc: use the right memchr. When retrieving Unicode text from an edit box in the GUI configurer, we were using plain memchr() to look for a terminating NUL. But of course you have to use wmemchr() to look for a UTF-16 NUL, or else memchr() will generate a false positive on the UTF-16 version of (at least) any ASCII character! (I also have to provide a fallback implementation of wmemchr for the w32old builds, which don't have it in the libc they build against. It's as simple as possible, and we use the libc version where possible.) --- cmake/cmake.h.in | 1 + cmake/platforms/windows.cmake | 1 + defs.h | 5 +++++ windows/CMakeLists.txt | 3 +++ windows/utils/getdlgitemtext_alloc.c | 4 +++- windows/utils/wmemchr.c | 15 +++++++++++++++ 6 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 windows/utils/wmemchr.c diff --git a/cmake/cmake.h.in b/cmake/cmake.h.in index 3d36b3d0..cb389cb6 100644 --- a/cmake/cmake.h.in +++ b/cmake/cmake.h.in @@ -15,6 +15,7 @@ #cmakedefine01 HAVE_GETNAMEDPIPECLIENTPROCESSID #cmakedefine01 HAVE_SETDEFAULTDLLDIRECTORIES #cmakedefine01 HAVE_STRTOUMAX +#cmakedefine01 HAVE_WMEMCHR #cmakedefine01 HAVE_DWMAPI_H #cmakedefine NOT_X_WINDOWS diff --git a/cmake/platforms/windows.cmake b/cmake/platforms/windows.cmake index 481809ec..e87f3380 100644 --- a/cmake/platforms/windows.cmake +++ b/cmake/platforms/windows.cmake @@ -53,6 +53,7 @@ define_negation(NO_HTMLHELP HAVE_HTMLHELP_H) check_include_files("winsock2.h;afunix.h" HAVE_AFUNIX_H) check_symbol_exists(strtoumax "inttypes.h" HAVE_STRTOUMAX) +check_symbol_exists(wmemchr "wchar.h" HAVE_WMEMCHR) check_symbol_exists(AddDllDirectory "windows.h" HAVE_ADDDLLDIRECTORY) check_symbol_exists(SetDefaultDllDirectories "windows.h" HAVE_SETDEFAULTDLLDIRECTORIES) diff --git a/defs.h b/defs.h index 0ac23ee2..0e56eb78 100644 --- a/defs.h +++ b/defs.h @@ -53,6 +53,11 @@ uintmax_t strtoumax(const char *nptr, char **endptr, int base); #define SIZEu "zu" #endif +#if !HAVE_WMEMCHR +/* Work around lack of wmemchr in older MSVC */ +wchar_t *wmemchr(const wchar_t *s, wchar_t c, size_t n); +#endif + #if defined __GNUC__ || defined __clang__ /* * On MinGW, the correct compiler format checking for vsnprintf() etc diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt index 960a440d..7d4fcadb 100644 --- a/windows/CMakeLists.txt +++ b/windows/CMakeLists.txt @@ -42,6 +42,9 @@ add_sources_from_current_dir(utils if(NOT HAVE_STRTOUMAX) add_sources_from_current_dir(utils utils/strtoumax.c) endif() +if(NOT HAVE_WMEMCHR) + add_sources_from_current_dir(utils utils/wmemchr.c) +endif() add_sources_from_current_dir(eventloop cliloop.c handle-wait.c) add_sources_from_current_dir(console diff --git a/windows/utils/getdlgitemtext_alloc.c b/windows/utils/getdlgitemtext_alloc.c index 8db32901..8de62a3b 100644 --- a/windows/utils/getdlgitemtext_alloc.c +++ b/windows/utils/getdlgitemtext_alloc.c @@ -4,6 +4,8 @@ * string is dynamically allocated; caller must free. */ +#include + #include "putty.h" char *GetDlgItemText_alloc(HWND hwnd, int id) @@ -27,7 +29,7 @@ wchar_t *GetDlgItemTextW_alloc(HWND hwnd, int id) do { sgrowarray_nm(ret, size, size); GetDlgItemTextW(hwnd, id, ret, size); - } while (!memchr(ret, '\0', size-1)); + } while (!wmemchr(ret, L'\0', size-1)); return ret; } diff --git a/windows/utils/wmemchr.c b/windows/utils/wmemchr.c new file mode 100644 index 00000000..7ccdfe3c --- /dev/null +++ b/windows/utils/wmemchr.c @@ -0,0 +1,15 @@ +/* + * Work around lack of wmemchr in older MSVC libraries. + */ + +#include + +#include "defs.h" + +wchar_t *wmemchr(const wchar_t *s, wchar_t c, size_t n) +{ + for (; n != 0; s++, n--) + if (*s == c) + return (wchar_t *)s; + return NULL; +} From 19798515df5db3151370e331ca1312bb39fadf16 Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Thu, 16 Jan 2025 07:24:06 +0000 Subject: [PATCH 39/44] ldisc_send: return early if len == 0. This can come up, for example, if the terminal receives a ^E character and has an empty answerback string configured. Without this early return, we append zero bytes to ldisc's ordinary bufchain input_queue, which is harmless; but we also append a zero-length record to ldisc's list of (type, length) chunks describing which parts of the input bufchain should be treated as interactive or as coming from special dedicated keystrokes (e.g. telling Return apart from ^M). That zero-length record is not _immediately_ harmful, but when the user next presses a key, it will have a different type from the empty answerback data, so that another chunk record is appended to the list after the zero-length one. And then ldisc_input_queue_callback goes into a tight loop, because it keeps trying to consume bytes from the start of the input bufchain but bounding the size at the length of the first (type, length) chunk, which is zero. So it consumes 0 bytes, finds the bufchain still isn't empty, and loops round again. --- ldisc.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ldisc.c b/ldisc.c index 899f5c82..eb7222c0 100644 --- a/ldisc.c +++ b/ldisc.c @@ -272,8 +272,10 @@ void ldisc_send(Ldisc *ldisc, const void *vbuf, int len, bool interactive) */ len = strlen(vbuf); type = DEDICATED; - } else { + } else if (len > 0) { type = interactive ? NORMAL : NONINTERACTIVE; + } else { + return; /* nothing to do anyway */ } /* From ec158a2e19c9f81b3676e157652d93955789f423 Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Sat, 18 Jan 2025 11:03:24 +0000 Subject: [PATCH 40/44] Pageant: call signop_unlink from signop_free. A user reported that the following sequence of events leads to Pageant crashing: - load an encrypted key into Pageant for decryption later - attempt to use the key, so that Pageant prompts for the passphrase - before entering the passphrase, abort the attempt to use the key (e.g. by closing the PuTTY that was trying to use it) - now enter the passphrase at the Pageant prompt, once the need for it has gone away. Once the key is decrypted, unblock_requests_for_key() goes through the linked list of blocked PageantSignOp attached to the private key record it's just decrypted, and tries to unblock them. The PageantSignOp belonging to the aborted Pageant request is still linked on that list, which it shouldn't be, because it's also been freed by pageant_unregister_client when that traversed the separate linked list of PageantAsyncOp associated with that client connection. So the private key's list of blocked requests contained a stale pointer. Now PageantSignOp's implementation of the PageantAsyncOp free method makes sure to unlink the signop from any list it's on before freeing it. --- pageant.c | 1 + 1 file changed, 1 insertion(+) diff --git a/pageant.c b/pageant.c index 961e1f03..6d6f70e1 100644 --- a/pageant.c +++ b/pageant.c @@ -648,6 +648,7 @@ static void signop_unlink(PageantSignOp *so) static void signop_free(PageantAsyncOp *pao) { PageantSignOp *so = container_of(pao, PageantSignOp, pao); + signop_unlink(so); strbuf_free(so->data_to_sign); sfree(so); } From 9ab416e0189181dee38103bb1e90df9b3ae5cb4a Mon Sep 17 00:00:00 2001 From: SATO Kentaro Date: Sun, 19 Jan 2025 22:42:49 +0900 Subject: [PATCH 41/44] request_file: fix wchar_t buffer length. [SGT: the helper function do_filereq_w expects its filename size to be in characters, not bytes, because it's used both as an index into the wchar_t buffer and also as nMaxFile in the OPENFILENAMEW structure which is also documented as measured in characters. So at the call site it should be measured via lenof rather than sizeof. This patch has done the same with the char version, which makes no functional difference but keeps the code more consistent.] --- windows/utils/request_file.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/windows/utils/request_file.c b/windows/utils/request_file.c index e9a94291..05ff2d63 100644 --- a/windows/utils/request_file.c +++ b/windows/utils/request_file.c @@ -235,13 +235,13 @@ Filename *request_file( char namebuf[MAX_PATH]; if (do_filereq_a( hwnd, title, initial, dir, save, filter, - NULL, namebuf, sizeof(namebuf))) + NULL, namebuf, lenof(namebuf))) filename = filename_from_str(namebuf); } else { wchar_t namebuf[MAX_PATH]; if (do_filereq_w( hwnd, title, initial, dir, save, filter, - NULL, namebuf, sizeof(namebuf))) + NULL, namebuf, lenof(namebuf))) filename = filename_from_wstr(namebuf); } @@ -349,14 +349,14 @@ struct request_multi_file_return *request_multi_file( char namebuf[MAX_PATH * MULTI_FACTOR]; if (do_filereq_a( hwnd, title, initial, dir, save, filter, - &first_filename_offset, namebuf, sizeof(namebuf))) + &first_filename_offset, namebuf, lenof(namebuf))) rmf = request_multi_file_populate_a( namebuf, first_filename_offset); } else { wchar_t namebuf[MAX_PATH * MULTI_FACTOR]; if (do_filereq_w( hwnd, title, initial, dir, save, filter, - &first_filename_offset, namebuf, sizeof(namebuf))) + &first_filename_offset, namebuf, lenof(namebuf))) rmf = request_multi_file_populate_w( namebuf, first_filename_offset); } From 8fb45f46173b38cd339aaba3b58cfe74b74b9f92 Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Sat, 1 Feb 2025 11:10:35 +0000 Subject: [PATCH 42/44] kex-hybrid: fix a small memory leak on failure. Spotted by Coverity: we've just allocated a strbuf to hold the output of the classical half of the hybrid key exchange, but if that output isn't generated due to some kind of failure, we forgot to free the strbuf on exit. --- crypto/kex-hybrid.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crypto/kex-hybrid.c b/crypto/kex-hybrid.c index e0c78743..8732432e 100644 --- a/crypto/kex-hybrid.c +++ b/crypto/kex-hybrid.c @@ -146,6 +146,7 @@ static bool hybrid_client_getkey(ecdh_key *ek, ptrlen remoteKey, BinarySink *bs) if (!ecdh_key_getkey(s->classical, classical_data, BinarySink_UPCAST(classical_key))) { ssh_hash_free(h); + strbuf_free(classical_key); return false; /* classical DH key didn't validate */ } s->alg->reformat(ptrlen_from_strbuf(classical_key), BinarySink_UPCAST(h)); @@ -238,6 +239,7 @@ static bool hybrid_server_getkey(ecdh_key *ek, ptrlen remoteKey, BinarySink *bs) if (!ecdh_key_getkey(s->classical, classical_data, BinarySink_UPCAST(classical_key))) { ssh_hash_free(h); + strbuf_free(classical_key); return false; /* classical DH key didn't validate */ } s->alg->reformat(ptrlen_from_strbuf(classical_key), BinarySink_UPCAST(h)); From d2c178c49a0ae6fa9ef75ca84fb3c9d0d675ea85 Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Sat, 1 Feb 2025 11:16:55 +0000 Subject: [PATCH 43/44] Update version number for 0.83 release. --- Buildscr | 2 +- LATEST.VER | 2 +- doc/plink.but | 2 +- doc/pscp.but | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Buildscr b/Buildscr index 497430db..c007da05 100644 --- a/Buildscr +++ b/Buildscr @@ -35,7 +35,7 @@ module putty ifeq "$(RELEASE)" "" set Ndate $(!builddate) ifneq "$(Ndate)" "" in . do echo $(Ndate) | perl -pe 's/(....)(..)(..)/$$1-$$2-$$3/' > date ifneq "$(Ndate)" "" read Date date -set Epoch 19052 # update this at every release +set Epoch 19120 # update this at every release ifneq "$(Ndate)" "" in . do echo $(Ndate) | perl -ne 'use Time::Local; /(....)(..)(..)/ and print timegm(0,0,0,$$3,$$2-1,$$1) / 86400 - $(Epoch)' > days ifneq "$(Ndate)" "" read Days days diff --git a/LATEST.VER b/LATEST.VER index e6e9cf41..08fec888 100644 --- a/LATEST.VER +++ b/LATEST.VER @@ -1 +1 @@ -0.82 +0.83 diff --git a/doc/plink.but b/doc/plink.but index d4bb183e..a985218f 100644 --- a/doc/plink.but +++ b/doc/plink.but @@ -41,7 +41,7 @@ use Plink: \c C:\>plink --help \c Plink: command-line connection utility -\c Release 0.82 +\c Release 0.83 \c Usage: plink [options] [user@]host [command] \c ("host" can also be a PuTTY saved session name) \c Options: diff --git a/doc/pscp.but b/doc/pscp.but index 94a5caf2..dd959121 100644 --- a/doc/pscp.but +++ b/doc/pscp.but @@ -39,7 +39,7 @@ use PSCP: \c C:\>pscp -h \c PuTTY Secure Copy client -\c Release 0.82 +\c Release 0.83 \c Usage: pscp [options] [user@]host:source target \c pscp [options] source [source...] [user@]host:target \c pscp [options] -ls [user@]host:filespec From d5329a9190e7a29c86751e95947e7674b1521a80 Mon Sep 17 00:00:00 2001 From: Larry Li Date: Wed, 28 May 2025 09:51:56 +0800 Subject: [PATCH 44/44] cleanup --- pscp.c | 1 - psftp.c | 1 - windows/plink.c | 1 - 3 files changed, 3 deletions(-) diff --git a/pscp.c b/pscp.c index 2f3bd1b9..fdfa367e 100644 --- a/pscp.c +++ b/pscp.c @@ -2235,7 +2235,6 @@ static void usage(void) printf(" -logoverwrite\n"); printf(" -logappend\n"); printf(" 记录文件已存在时覆盖文件还是在文件末尾添加内容\n"); - cleanup_exit(1); } void version(void) diff --git a/psftp.c b/psftp.c index 46c18bce..cbe7e685 100644 --- a/psftp.c +++ b/psftp.c @@ -2564,7 +2564,6 @@ static void usage(void) printf(" -logoverwrite\n"); printf(" -logappend\n"); printf(" 记录文件已存在时覆盖文件还是在文件末尾添加内容\n"); - cleanup_exit(1); } static void version(void) diff --git a/windows/plink.c b/windows/plink.c index 9ea89d12..52cf915c 100644 --- a/windows/plink.c +++ b/windows/plink.c @@ -187,7 +187,6 @@ static void usage(void) printf(" 记录文件已存在时覆盖文件还是在文件末尾添加内容\n"); printf(" -shareexists\n"); printf(" 测试是否存在上游连接共享\n"); - exit(1); } static void version(void)