Skip to content

PPC64LE DSA: divide X by q over q's digits in parameter generation - #4718

Closed
alex wants to merge 1 commit into
claude/ppc64le-dsa-prime-r2from
claude/ppc64le-dsa-parith
Closed

alex wants to merge 1 commit into
claude/ppc64le-dsa-prime-r2from
claude/ppc64le-dsa-parith

Conversation

@alex

@alex alex commented Nov 8, 2026 •

Copy link
Copy Markdown
Member

Part of #4141 (finding 7 of the PPC64LE audit: DSA parameter generation). Stacked on #4709, which is stacked on #4678 (in the merge queue), so this stays a draft until those merge. It changes no Spec/, TCB/ or RSA file, and no Rust code outside the generated PPC64LE crate.

What was slow

Each p candidate's pArith computes p = X − (X mod 2q) + 1 and (p − 1)/q by dividing X by q. It used the RSA key operations' bit-serial division (Impl/Rsa/PPC64LE/Div.lean), which works over p's full width:

  • 32·w steps, where w is the number of 32-bit digits of p.
  • Each step makes four passes over p's width: it shifts the quotient and the remainder, subtracts the divisor from the remainder, and selects the result.

But the remainder is always below q, which is at most 256 bits.

What changed

Impl/Dsa/PPC64LE/DivN.lean's div q r d t v, where v = q_len / 4 is q's number of digits:

  • It runs the same 32·w steps and shifts the quotient over its w digits as before.
  • It shifts, subtracts from and selects the remainder over v + 1 digits (6 or 9) instead of w + 1 (33 to 97).
  • At the end it zeroes the remainder's digits from v + 1 up, so array aRm holds the remainder as a w-digit number. The rest of pArith is unchanged.

Its loop counts depend only on w and the constant v, as before.

Proofs (Proof/Dsa/PPC64LE/Gen/KSDiv.lean):

  • DivN.div_ok follows Proof/Rsa/PPC64LE/DivLoop.lean's proof of the RSA division. It uses the same divStep/divIter iteration and the same loop lemmas (shlLoop_ok, subLoop_ok, selLoop_ok, zeroLoop_ok).
  • The remainder is over v + 1 digits (numV). step_arith takes the divisor's bound separately from the quotient's.
  • div_vp takes q < 2^(32 v) instead of TopAll. pDivide_vp gets that bound from q's q_len octets.
  • The taint certificate is checked for each of the two q lengths (20 and 32 octets).

Numbers

Benchmarks run 42521443256: POWER9, dsa_generate_parameters, base = #4709's head (0edfcd7), one run of each side.

Benchmark Base Head Change OpenSSL Head vs OpenSSL
dsa_generate_parameters/1024 136.59 ms 127.90 ms 6.4% faster 81.55 ms 1.6×
dsa_generate_parameters/2048 945.68 ms 1034.16 ms 9.4% slower 662.13 ms 1.6×
dsa_generate_parameters/3072 6678.74 ms 6448.60 ms 3.4% faster 2358.03 ms 2.7×

This is a smaller gain than I expected. I had estimated the division at 20–25% of parameter generation, and that estimate was too high.

The 2048-bit row is noise on the head side, not a slowdown:

  • The head's samples have a standard deviation of 359 ms, and their mean's 95% interval runs from 976 ms to 1.41 s. The base's standard deviation is 10 ms.
  • The code this PR changes does strictly less work per step at every size, and its loop structure is the same.

I have not rerun it, per the benchmarking rules. A reviewer may want a scoped run of the 2048-bit group before merging.

Most of parameter generation is still the Miller–Rabin rounds, which belong to the bignum work. Shifting the quotient over p's full width is now the main cost left in this division. Reading the dividend's bits in place instead would remove that, but it needs new step math, and on these numbers it would gain little.

Checks

  • Restacked onto main after PPC64LE DSA: signing, key generation and key checks by four-bit windows (powWin) #4667 merged. lake build of the whole project passes, and the full emitter leaves the tree clean. It regenerates only dsa.rs/dsa.s of the PPC64LE crate (vg_dsa_parameter_search).
  • The Static checks pass. check_lean_public_imports.py is OK on the edited modules.
  • cargo check --target powerpc64le-unknown-linux-gnu --lib builds the crate. CI's ppc64le jobs run the tests.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TKUFHEf743DNZVAyToasZ6

`pArith` divides X (p_len octets) by q with the RSA key operations'
bit-serial division, which shifts, subtracts from and selects the
remainder over all w + 1 digits of p's width in each of its 32 w steps.
The remainder stays below q, which has v = q_len / 4 digits, so
`Impl/Dsa/PPC64LE/DivN.lean`'s division keeps the quotient's shift over w
digits but runs the remainder's shift, subtraction and selection over
v + 1 digits. It zeroes the remainder's digits from v + 1 at the end, so
that its array holds the remainder as a w-digit number as before.

`DivN.div_ok` (in `Proof/Dsa/PPC64LE/Gen/KSDiv.lean`) proves it as
`Proof/Rsa/PPC64LE/DivLoop.lean` proves the RSA division, with the same
`divStep` iteration and loop lemmas. `pDivide_vp` gives it q < 2^(8 q_len).
No `Spec/`, `TCB/` or RSA file changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TKUFHEf743DNZVAyToasZ6
@alex
alex force-pushed the claude/ppc64le-dsa-prime-r2 branch from abfa4cd to 0edfcd7 Compare November 8, 2026 20:41
@alex
alex force-pushed the claude/ppc64le-dsa-parith branch from 3b7b10c to a265731 Compare November 8, 2026 20:41

alex commented Nov 8, 2026

Copy link
Copy Markdown
Member Author

Closing this: the gain doesn't justify the code.

It adds a DSA-only division module with about 400 lines of proof. In return, parameter generation was 3–6% faster at 1024 and 3072 bits, and the 2048-bit result was too noisy to read (run 42521443256). I had estimated the division at 20–25% of parameter generation, which turned out to be too high. Most of the time is in the Miller–Rabin rounds.

#4709 (the Miller–Rabin R² setup by r2Hi) stays open. It is small and reuses existing verified code.


Generated by Claude Code

@alex alex closed this Nov 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

2 participants