Skip to content

Ct fixes - #1

Merged
dcharlot65-openie merged 7 commits into
mainfrom
ct-fixes
Oct 16, 2026
Merged

dcharlot65-openie merged 7 commits into
mainfrom
ct-fixes

Conversation

@dcharlot65-openie

Copy link
Copy Markdown
Contributor

No description provided.

David Charlot 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.
@dcharlot65-openie
dcharlot65-openie merged commit f39f9f0 into main Oct 16, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant