Skip to main content

decaps

Function decaps 

Source
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 dkH(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.