pub fn decaps<P: Params>(
dk: &[u8],
c: &[u8],
_rng: &mut impl CryptoRng,
) -> Result<[u8; 32], MlKemError>Expand description
Algorithm 21: ML-KEM.Decaps(dk, c) with input validation and DFA protection.
§Side-channel / fault countermeasures
Three independent countermeasures are layered inside this function.
Full threat-model context in doc/sca/countermeasures/ml_kem.rst,
sections DFA — double computation + CT fault fallback and
DFA on dk — H(ek) integrity check.
§1. dk integrity check — fault on dk in storage
The decapsulation key’s FIPS 203 layout is
dk_pke ‖ ek ‖ H(ek) ‖ z. A fault that alters dk in memory
(for example a hot-carrier-induced bit flip in flash) would
undermine the FO security argument because the attacker could
coerce decapsulation into using a crafted dk_pke. We recompute
H(ek_in_dk) and compare it with the stored H(ek) using
silentops::ct_eq; a mismatch aborts with
MlKemError::InvalidDecapsulationKey before decaps_internal
runs.
§2. Double computation — fault on FO re-encryption
ML-KEM decapsulation is vulnerable to a classical DFA on the FO re-encryption step: if an attacker can make the re-encryption return a value close to the real ciphertext on one specific input, the implicit-rejection path is bypassed and the KEM acts as a decryption oracle (Boneh–DeMillo–Lipton, EUROCRYPT 1997).
We run decaps_internal_sca twice. A single-fault attacker can
only affect one execution; if the two shared secrets differ,
we conclude a fault happened and switch to the constant-time
fallback described below. In the no-fault path both runs agree
and we return either result.
§3. Constant-time fault fallback — leakage on the fault branch
Naive code would write
if !results_match { return k_fault }, which introduces a
conditional jump on a secret-derived bit (results_match is
derived from k1, k2 which both depend on the sk). An
attacker able to inject a fault AND measure timing learns
“fault was detected” from the branch timing alone.
To close this, we always compute k_fault = SHA3(z ‖ 0xFF)
and select between k1 and k_fault with
[silentops::ct_select_u8] (via the local 32-byte ct_select
wrapper). The branch is gone; timing is identical in both
cases. k_fault is:
- deterministic for a given
(dk, c)so a repeated faulted call returns the same value (prevents oracle-by-repetition); - distinct from both the legitimate FO output and the implicit-
rejection output (
J(z ‖ c)), so the attacker cannot distinguish “fault detected” from either correct branch.
Recommended for embedded and high-security contexts where physical fault attacks are in the threat model.