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);