Repository navigation
Ct fixes - #1
Merged
Merged
Ct fixes#1
Conversation
added 7 commits
October 16, 2026 05:39
crypto.open, hpke.gcm_open and pq.mlkem_decaps compared hex text with ==, which may exit at the first differing character. Add ct_eq (and self-contained copies hp_ct_eq, kem_ct_eq so each file still checks against an embedded crypto that predates ct_eq): XOR every byte pair, OR into one accumulator, test once. Only the length is public. Add scripts/ct_check.sh (vector checks and a fuel-noninterference harness over rce emit python --fuel-trace) with tests/crypto/tag_*. Source-level and constant-fuel only: fuel counts evaluated expressions, so it cannot see the early exit of a native text compare. No wall-clock constant-time claim.
The Montgomery ladder swapped (a,b) and (c,d) with 'if r == 1' before and after each step, a branch on every scalar bit (RFC 7748 5.1 asks for a constant-time cswap). Swap with the mask m = 0 - bit: d = m & (a ^ b) per limb, a ^= d, b ^= d, all limbs always touched. Tests: RFC 7748 5.2 (both vectors), iterated test round 1, 6.1 DH (both directions), plus 9 differential vectors from OpenSSL via the Python cryptography package. Fuel test: five different scalars (zero, all ones, alternating, two RFC scalars) use exactly 13900894 fuel each; the parent commit used 13528634 to 13538674 depending on the scalar (negative control, so the ladder branch showed in fuel). Source-level only.
hexval scanned a digit table with a branch per entry, tohex indexed a digit table with secret nibbles. Both are on every secret path (keys, scalars, shared secrets pass through unhex and tohex). hexval now builds masks from the sign of range differences; tohex computes 48 + n + (39 & mask(n > 9)) and builds text with utf8(). Behaviour kept: non lowercase hex reads as 0, odd trailing digit ignored, bytes outside 0..255 trap (via utf8 of a guard element, no data branch). Tests: all 256 bytes both ways, class boundaries, legacy cases, and a fuel group over differing digits. Honest note: the old hexval also used equal fuel (its branch body ran once per digit), so fuel cannot show this fix; the gain is the absence of a secret-indexed table and branch, a source-level property only.
pt_mul indexed a 16-entry table with nibbles of the secret scalar (key generation, and the secret nonce in signing). pt_select now reads every entry and keeps the match with a mask built from (j ^ n) - 1, so no table index depends on the scalar. pt_mul2 (verification, public scalars) is unchanged. sc_reduce branched on 'bge' (early return) and on a per-limb borrow. sub_borrow computes r - L with the borrow as a sign bit and sc_reduce keeps r or r - L by mask; bge is now the borrow of sub_borrow (also used on public data in ed_verify). Tests: RFC 8032 7.1 TEST 1, 2, 3, SHA(abc) (public key, signature, verify, one operation per file for comptime fuel) cross-checked against OpenSSL, 2 more differential vectors, 4 refusal cases (flipped message, flipped signature, S + L, short key), sc_reduce and sc_muladd against Python big integers, bge edge cases. Fuel groups: ed_pubkey (4 seeds) 15168109 each, ed_sign (3 seeds) 33806900 each, sc_reduce (4 inputs) 1113938 each. The parent code differed with the input (sc_reduce 419917 to 582250, ed_sign 30493688 to 30521457). Cost: pubkey +5 percent, sign +11 percent fuel, sc_reduce about 2.6x. Source-level only: list indexing is one fuel step for any index, so fuel does not witness cache behaviour.
… checks - Every '% 3329' and the '/ 3329' in compress, on secret-derived values (the KyberSlash pattern), becomes qdiv/qmod: (n * 330282857) >> 40, exact for all n < 346,084,868 (error term 3177; uses stay below 2^25). - Decaps compares the re-encryption with a branch-free OR-of-XOR and chooses the real key or the rejection key with a mask, kbar ^ (mask & (K ^ kbar)), instead of 'if equal'. - FIPS 203 7.2: encaps refuses (returns "") an ek of the wrong length or failing the modulus check ByteEncode12(ByteDecode12(t)) == t, and an m that is not 32 bytes. FIPS 203 7.3: decaps refuses a ciphertext or dk of the wrong length and a dk whose stored H(ek) does not match. A caller must not feed "" onward as a secret. Tests: known answers for keygen, encaps, decaps and implicit rejection (2 vectors each) from the independent kyber-py FIPS 203 implementation; exhaustive compress over x in 0..3328 for d = 1, 4, 5, 10, 11 against the exact rounded division; qdiv/qmod boundaries; 8 refusal cases. Fuel: one key, valid vs three rejected ciphertexts, 15580440 each (parent: 13355039 valid vs 13354447 rejected). Source-level only; the division is gone but multiply latency on the host is unverified.
…check - AES SubBytes and key expansion: the S-box table indexed by secret bytes is replaced by the GF(2^8) inverse (a^254) plus the FIPS 197 affine map, with a mask-based gmul. The table now lives only in the test as the reference (all 256 inputs agree). xt uses a mask from the top bit. - GHASH gf_mul: the two 'if bit' branches become masks (0 - bit). - RFC 9180 7.1.4: hpke_seal and hpke_open return "" when the DH output is all zero (OR of all bytes, one test), and for a public key or enc that is not 32 bytes. MLS EncryptWithLabel/DecryptWithLabel inherit this. Tests: RFC 9180 A.1 in pieces (DeriveKeyPair, pubkeys, shared secret from both sides, key schedule, GCM seal for sequence numbers 0 to 3, full hpke_seal and hpke_open), tampering refusals, low-order point refusals, sbox/gmul/xt/ginv. Fuel groups: aes_block 848394, gcm_seal 2482594, gf_mul 92276 for differing keys and data. Parent: gcm_seal 250368 to 258669, gf_mul 60285 to 87465 (the branches). Cost: AES block fuel is about 50x (16918 to 848394) because SubBytes is computed, not looked up. Source-level only; slow in an interpreter by design, the report's host-intrinsic route is the practical one for production.
mlkem_decaps now answers "" for an input FIPS 203 7.3 makes it refuse. Derive nothing from an empty secret.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.