From e98615f0ba2d20607b13169e4dd9966da082139c Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Sat, 7 Dec 2024 19:33:39 +0000 Subject: [PATCH] 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 @@ LzN/Ly+uECsga2hoc+P/ZHMULMZkCfrOyWdeXz7BR/acLZJoT579 # 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 @@ class standard_test_vectors(MyTestBase): '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);