Skip to content

Luis/utils/ct unsigned masks#63

Open
laruizlo wants to merge 5 commits into
bcgit:release/0.1.3alphafrom
laruizlo:luis/utils/ct-unsigned-masks
Open

Luis/utils/ct unsigned masks#63
laruizlo wants to merge 5 commits into
bcgit:release/0.1.3alphafrom
laruizlo:luis/utils/ct-unsigned-masks

Conversation

@laruizlo

Copy link
Copy Markdown
Collaborator

Extend ct.rs Condition to unsigned widths (u64 parity, new u32)

Why

Groundwork for the upcoming RSA implementation. The RSA bigint layer uses u64 limbs on 64-bit targets and u32 limbs elsewhere, and needs identical constant-time mask constructors for both widths so the dual-width code is written once. Today Condition support is uneven: Condition<i64> has the full constructor set, Condition<u64> has only from_bool/select/is_true, and Condition<u32> does not exist.

This PR is deliberately separate from the RSA work so the shared security-critical utils code gets its own review, and the RSA PRs stay pure-RSA.

What

  • Register u32 (and i32 for width symmetry) in supported_mask_type!. Registration alone gives the new widths the existing generic boolean operator impls (& | ^ !).
  • One macro-generated impl brings Condition<u64> and Condition<u32> to constructor parity where meaning is width-independent:
    • TRUE / FALSE
    • from_bool, from_bool_var: mask from a boolean (wrapping-sub construction)
    • from_lsb: mask from bit 0 (parity/oddness tests)
    • from_msb: mask from the top bit (adaptor for borrow/carry words coming out of wrapping subtraction chains: from_msb(borrow) is the lt mask, no post-processing)
    • is_zero, is_not_zero, is_equal
    • select, mov, swap, to_bool_var
    • all const fn except mov
  • Condition<i64> is untouched. Condition<u64>::select becomes const (previously a plain fn, additive change); is_true is kept for backward compatibility with a doc note pointing at to_bool_var.

IMPORTANT / API removal: commit 0202982 removes Condition<u64>::is_true(). It duplicated to_bool_var() under a second name and was the API's only &self receiver; the standardized accessor across all Condition widths is now to_bool_var(). A workspace-wide search found no production callers (its only uses were this crate's own tests, migrated in the same commit), so nothing downstream breaks, but any out-of-tree or in-flight code calling is_true() must switch to to_bool_var() if the current PR is accepted as is. The removal is deliberately isolated in its own commit: if reviewers prefer to keep is_true, dropping that single commit restores it without affecting the rest of the PR.

Design notes for review

  • The unsigned constructions do NOT reuse the i64 shapes. The i64 constructors rely on signed-representation tricks (is_negative via arithmetic shift, is_lt via the sign of x - y) whose overflow reasoning is invalid for full-range unsigned values. The unsigned versions use the standard two's-complement mask identities: wrapping_sub from a 0/1 bit, and MSB-extraction of x | x.wrapping_neg() for the zero test.
  • Ordering comparisons are deliberately omitted from the unsigned set. Multi-word callers (the RSA bigint) derive lt from their subtraction borrow chain and convert with from_msb; a word-level unsigned is_lt would invite exactly the misuse the i64 trick warns about.
  • Constructors are const fn, matching the i64 style; black_box hygiene stays where it already lives (the byte-level helpers), since black_box is not const-compatible and the existing i64 mask constructors don't use it either.

Tests

Macro-generated test suites keep u64 and u32 at exact behavioural parity:

  • Boundary coverage at 0, 1, MAX, MAX-1, 1 << (BITS-1) for every constructor, the values where leaked signed-representation reasoning would produce wrong masks.
  • Mask-canonicality assert on every constructor result: select(MAX, 0) must return exactly MAX or 0, proving the mask is all-ones/all-zeros through the public API (kills wrong-mask mutants).
  • A borrow-adaptor cross-check: from_msb of a widening-subtraction borrow word agrees with the < operator over all boundary pairs.
  • Existing i64/u64 tests untouched and passing.

Verified additionally against a dual-width bigint prototype implementing the RSA phase-1 predicate/conditional set (ct_is_zero, ct_eq, ct_lt via sbb chain, is_odd, bit(i), select/assign/swap, conditional modular correction, masked table scan) — compiles and passes written-once against Condition<Word> in both the native-u64 and forced-u32 lanes.

Consumers

hex, base64, mlkem, mlkem-lowmemory use exclusively Condition<i64> constructors, untouched code paths. bouncycastle-utils tests: 69/69 pass. Quality stats: unwrap/Err() counts flat (491/260 core-code baseline).

Pre-push verification:

  • cargo test --workspace passed
  • cargo mutants over crypto/utils
    • XOR/OR-equivalence survivors triaged per house rules
    • crypto/utils/src/secret.rs:293:9: replace <impl Drop for Secret<T>>::drop with ()

Non-goals, considered and deferred

  • The file's own "generic impl<T>" TODO — worthwhile, but a refactor of shared security-critical code beyond what this feature needs.
  • Condition<u8> for the hex/base64 TODO — same direction as this work; can follow once the shape here is agreed.
  • The is_in_list compiler-CT research question — the new code sidesteps it by using only the mask shapes.

Observed during review, out of scope for this PR

While aligning the unsigned API with the existing Condition<i64> surface, a few pre-existing inconsistencies were noted. They are listed here so reviewers know they were seen and deliberately left alone; each is small and could be a standalone cleanup:

  • is_bit_set(value: i64, bit: i64) types its bit index as i64. The new unsigned variant uses u32, following core's shift-count convention. The signed one should eventually migrate, which is a breaking signature change.
  • or_halves is public but is an implementation detail. It returns a raw i64 rather than a Condition, exists only to build is_zero, and has no callers outside this file. It should become private.
  • is_lte, is_gte, and is_within_range are not const because they use the non-const Not operator on Condition. The unsigned is_zero sidesteps this by complementing the inner value instead; the same trick would make the signed trio const with no behavior change.
  • negate's doc comment is a leftover bug-investigation narrative. It walks through a wrong result under the assumption TRUE = 1 and then notes the constant was changed; next to TRUE: Self = Self(-1) it now confuses more than it explains. It needs a rewrite stating the current contract.
  • conditional_copy_bytes takes take_a: bool and hand-builds its own mask instead of accepting a Condition, and the byte-helper naming (ct_eq_bytes) does not match the method vocabulary (is_equal). This is best revisited together with the existing Condition<u8> TODO, which would let the byte helpers consume proper masks.
  • Most Condition<i64> items carry empty /// doc comments, satisfying forbid(missing_docs) in letter only. This PR filled in is_bit_set and is_negative where it added cross-references; the rest still need real text.

Co-authored with Claude Code

laruizlo and others added 5 commits July 24, 2026 18:24
Groundwork for the RSA bigint layer: identical mask constructors for u64/u32 limbs so dual-width code is written once.

- Register u32 (and i32 for symmetry) in supported_mask_type!.
- Macro-generated impl for u64/u32 parity: from_bool, from_bool_var, from_lsb, from_msb, is_zero, is_not_zero, is_equal, select, mov, swap, to_bool_var (const fn except mov).
- Unsigned constructions use two's-complement mask identities, not the i64 sign tricks, which are invalid for full-range unsigned values.
- Ordering comparisons deliberately omitted: callers derive lt from subtraction borrow chains via from_msb.
- Condition<u64>::select becomes const; is_true kept for backward compatibility; Condition<i64> untouched.
- Tests: parity suites for both widths with boundary coverage, mask-canonicality asserts, and a borrow-adaptor cross-check against the < operator.

Non-goals deferred: generic impl<T> refactor, Condition<u8>, is_in_list CT research question.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s_bit_set

- is_negative and from_msb rustdoc now point at each other as the signed/unsigned spellings of the top-bit mask; is_bit_set and from_lsb likewise for the bit-0 case.
- New is_bit_set(value, bit) on Condition<u64>/Condition<u32> for shape parity with the signed impl, delegating to from_lsb; index type follows the u32 shift-count convention of core.
- Real doc text on the previously empty is_bit_set/is_negative doc comments.
- Tests: is_bit_set agrees with from_lsb at bit 0 and from_msb at the top bit, and each single-bit value reports exactly its own bit, both widths.
- Comment style fix in the i32 registration note.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Remove Condition<u64>::is_true, which duplicated to_bool_var under a different name and the only &self receiver in the API; its sole callers were this crate's tests.
- One accessor across all widths keeps the is_* prefix meaning "data in, mask out", preserves the from_bool_var/to_bool_var boundary pair, and keeps the _var suffix warning that converting to bool exits constant-time discipline.
- Migrated the u64 test call sites accordingly; no production callers existed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- i64 mask-canonicality test: select(-1, 0) must return exactly -1 or 0, catching the delete-unary-minus mutants in from_bool_var and is_bit_set that truthiness checks let survive (same shape as the unsigned suites' assert_canonical).
- ct_eq_zero_bytes had no tests; added zero/nonzero coverage at first, last, and high-bit positions, killing all four of its mutants including the or-assign to and-assign fold corruption.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Formatting-only sweep of pre-existing drift in 21 files (trailing whitespace, blank-line collapses, rewraps); zero code changes (git diff -w shows only 4 blank-line deletions). Needed because rust-style.yml runs cargo fmt --all --check on every PR, which gates on files this branch never touched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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