Commit febac4fbf3 for openssl.org
commit febac4fbf34d6506154795b91a9610da905f1fcb
Author: Viktor Dukhovni <openssl-users@dukhovni.org>
Date: Fri Mar 27 04:02:34 2026 +1100
Refactor ML-KEM decap, also cleanse failure_key
Pedantically cleanse the typically unused decap failure_key's stack
copy.
When actually used, it is copied into the caller's shared secret result,
perhaps to be cleansed there after use, or not, that's the callers
business.
While at it, slightly refactor the internal decap() implementation to
consolidate all the data to be cleansed into a single buffer, but now
avoid copying the public key hash, instead, when computing "K || r" as
"G(m || h)" include "h" via a separate EVP_DigestUpdate() call.
Reviewed-by: Paul Dale <paul.dale@oracle.com>
Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
MergeDate: Tue Mar 31 05:19:40 2026
(Merged from https://github.com/openssl/openssl/pull/30598)
diff --git a/crypto/ml_kem/ml_kem.c b/crypto/ml_kem/ml_kem.c
index babc2281c1..9ea824ccff 100644
--- a/crypto/ml_kem/ml_kem.c
+++ b/crypto/ml_kem/ml_kem.c
@@ -1448,6 +1448,30 @@ static int encap(uint8_t *ctext, uint8_t secret[ML_KEM_SHARED_SECRET_BYTES],
return ret;
}
+/*
+ * Hash the input message |m'| and public key digest |h|
+ * to obtain |K| and |r|.
+ */
+static int hash_kr(uint8_t *out, uint8_t *in,
+ EVP_MD_CTX *mdctx, const ML_KEM_KEY *key)
+{
+ unsigned int sz, wanted;
+
+ wanted = ML_KEM_SHARED_SECRET_BYTES + ML_KEM_RANDOM_BYTES;
+ return (EVP_DigestInit_ex(mdctx, key->sha3_512_md, NULL)
+ && EVP_DigestUpdate(mdctx, in, ML_KEM_RANDOM_BYTES)
+ && EVP_DigestUpdate(mdctx, key->pkhash, ML_KEM_PKHASH_BYTES)
+ && EVP_DigestFinal_ex(mdctx, out, &sz)
+ && ossl_assert(sz == wanted));
+}
+
+/*-
+ * Decap needs space for: Kbar | K | r | m'
+ * We slice up a single buffer to hold them all.
+ * We don't need to cleanse the public pkhash value.
+ */
+#define DECAP_BUFFER_SZ (2 * ML_KEM_SHARED_SECRET_BYTES + 2 * ML_KEM_RANDOM_BYTES)
+
/*
* FIPS 203, Section 6.3, Algorithm 18: ML-KEM.Decaps_internal
*
@@ -1463,11 +1487,11 @@ static int decap(uint8_t secret[ML_KEM_SHARED_SECRET_BYTES],
const uint8_t *ctext, uint8_t *tmp_ctext, scalar *tmp,
EVP_MD_CTX *mdctx, const ML_KEM_KEY *key)
{
- uint8_t decrypted[ML_KEM_SHARED_SECRET_BYTES + ML_KEM_PKHASH_BYTES];
- uint8_t failure_key[ML_KEM_RANDOM_BYTES];
- uint8_t Kr[ML_KEM_SHARED_SECRET_BYTES + ML_KEM_RANDOM_BYTES];
+ uint8_t buf[DECAP_BUFFER_SZ];
+ uint8_t *failure_key = buf; /* Kbar */
+ uint8_t *Kr = failure_key + ML_KEM_SHARED_SECRET_BYTES;
uint8_t *r = Kr + ML_KEM_SHARED_SECRET_BYTES;
- const uint8_t *pkhash = key->pkhash;
+ uint8_t *m = r + ML_KEM_RANDOM_BYTES; /* m' */
const ML_KEM_VINFO *vinfo = key->vinfo;
int i;
uint8_t mask;
@@ -1493,20 +1517,18 @@ static int decap(uint8_t secret[ML_KEM_SHARED_SECRET_BYTES],
vinfo->algorithm_name);
return 0;
}
- decrypt_cpa(decrypted, ctext, tmp, key);
- memcpy(decrypted + ML_KEM_SHARED_SECRET_BYTES, pkhash, ML_KEM_PKHASH_BYTES);
- if (!hash_g(Kr, decrypted, sizeof(decrypted), mdctx, key)
- || !encrypt_cpa(tmp_ctext, decrypted, r, tmp, mdctx, key)) {
+ decrypt_cpa(m, ctext, tmp, key);
+ if (!hash_kr(Kr, m, mdctx, key)
+ || !encrypt_cpa(tmp_ctext, m, r, tmp, mdctx, key)) {
memcpy(secret, failure_key, ML_KEM_SHARED_SECRET_BYTES);
- OPENSSL_cleanse(decrypted, ML_KEM_SHARED_SECRET_BYTES);
- return 1;
+ goto end;
}
mask = constant_time_eq_int_8(0,
CRYPTO_memcmp(ctext, tmp_ctext, vinfo->ctext_bytes));
for (i = 0; i < ML_KEM_SHARED_SECRET_BYTES; i++)
secret[i] = constant_time_select_8(mask, Kr[i], failure_key[i]);
- OPENSSL_cleanse(decrypted, ML_KEM_SHARED_SECRET_BYTES);
- OPENSSL_cleanse(Kr, sizeof(Kr));
+end:
+ OPENSSL_cleanse(buf, DECAP_BUFFER_SZ);
return 1;
}