Skip to content

Route large bignum arithmetic through OpenSSL - #20

Merged
derek-miller merged 6 commits into
mainfrom
claude/openssl-big-number-support-2a0c40
Oct 18, 2026
Merged

derek-miller merged 6 commits into
mainfrom
claude/openssl-big-number-support-2a0c40

Conversation

@derek-miller

@derek-miller derek-miller commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Depends on #19. This branch's first commit is #19. Merge that first; once it lands, this diff shrinks to the second commit.

What changes

crypto.bignum already routed mod_exp through lua-openssl's openssl.bn. Now mul, divmod, mod and mod_mul do too, using the same machinery mod_exp used:

  • Inputs and outputs stay the canonical limb table: bytes in through bn.text, hex out through bn.tohex.
  • A binding is trusted only after a multi-limb known-answer check, now run per operation, so one broken entry point doesn't take mod_exp down with it.
  • Any failure in the binding is caught by a pcall and falls back to pure Lua.

bignum.is_accelerated(op) takes an optional operation (default mod_exp), so srp.is_accelerated() is unchanged.

When it routes

The OpenSSL side's cost is almost all conversion, which is linear in the limbs moved. So an operation routes only when its estimated pure-Lua work reaches 4 limb products per limb converted in or out. Calls whose pure-Lua cost is linear stay in Lua at any width:

  • add and sub
  • a one-limb factor or divisor
  • a quotient of one or two limbs
  • operands already reduced

Pure-Lua time ÷ OpenSSL time, including conversions (lua-openssl 0.8.5, Apple Silicon):

call Lua 5.4 LuaJIT LuaJIT -joff
mul 128x128 limbs (3072 bits) 16.3x 5.2-6.0x 11.8x
divmod 256/128 14.1x 5.2-7.4x 15.7x
mod_mul 128,128,128 29.5x 12.7-13.4x 25.2x
SRP's mod_mul(k, v, N) 8.8x 3.6x 6.8x
add/sub, reduced mod_add/mod_sub (kept in Lua) 0.0-0.3x 0.0-0.2x 0.2-0.3x

One known trade-off. With LuaJIT's JIT on, a routed mul whose narrower factor is under about 24 limbs runs about 2x slower than pure Lua. That covers SRP's u*x, about 2 µs. Under Lua 5.4 and LuaJIT -joff the same shapes win 1.6-2.7x. Nothing records whether Control4's LuaJIT runs its JIT, so the rule doesn't assume either way. The full table and reasoning are in the comment that opens bignum.lua's OpenSSL section.

Binding quirks guarded against

Measured on the 0.8.5, 0.9.2 and 0.11.1 rocks:

  • divmod, mod, mulmod and powmod return 0 for a zero divisor instead of raising.
  • tohex writes negative values as -….

Both are refused, so the call falls back to pure Lua, which reports the proper error.

Verification

  • Differential fuzzing: about 23,000 cases comparing acceleration on vs off against the real 0.8.5, 0.9.2 and 0.11.1 rocks on Lua 5.4, plus 0.8.5 on LuaJIT and the built crypto-portable.lua. Results were cross-checked against Python. Every routing decision matched the documented rule exactly.
  • Mutation testing: 18 targeted mutants of the routing, check, fallback and guard code are all caught by the selftest.
  • The selftest's real-binding test runs whenever a binding is installed. It requires every operation the binding is trusted with to have actually returned the binding's answer.
  • CI's openssl-matrix job now asserts is_accelerated(op) for all five operations on each real rock.

Test plan

  • make check clean
  • ./run_tests.sh passes on Lua 5.4 with each of the three rocks (acceleration on and off), LuaJIT with 0.8.5 (on and off), and Lua 5.5
  • make test-matrix (Lua 5.1-5.4, LuaJIT)
  • CI

bignum.sub returned a wrong value instead of raising when the subtrahend had
more limbs than the minuend: sub_raw only read b to a's width, so a wider b
whose low limbs happened not to borrow slipped through. It now raises on a
wider subtrahend, and the tests cover that case and equal-width underflow.

The bignum and random selftests restored the acceleration flag to the
CRYPTO_USE_OPENSSL default rather than the caller's setting, so a driver that
called crypto.use_openssl(true) and then crypto.selftest() was left
unaccelerated, which on Control4 makes SRP unusable. openssl_wrapper.enabled()
exposes the flag so they can hand the caller's choice back, crypto.selftest()
fails any module that leaves it changed, and the runner's lpack pass now runs
crypto.selftest() with the flag flipped from the environment default.
mod_exp was the only bignum operation that used lua-openssl. mul, divmod,
mod and mod_mul now do too when the estimated pure-Lua work reaches four limb
products per limb converted in or out, which is where the bytes-in/hex-out
conversion pays for itself: at 3072 bits they run 5-30x faster. Calls whose
pure-Lua cost is linear (add, sub, a one-limb factor or divisor, a narrow
quotient, reduced operands) stay in Lua, where they are faster at every size
measured.

Each routed operation must reproduce its own multi-limb known answer before
the binding is trusted with it, results are unchanged either way, and
bignum.is_accelerated(op) reports per operation. CI asserts all five on every
real lua-openssl rock.
svc-finitelabs[bot]
svc-finitelabs Bot previously approved these changes Oct 17, 2026

@svc-finitelabs svc-finitelabs Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed b91226f, scoped to the second commit; 73edb20 is #19 and is reviewed there.

The routing matches the rule in the body. A zero divisor never reaches the binding: div_work is 0 for it in divmod/mod, and mod_mul's require_nonzero raises inside the pcall so the pure-Lua error comes through. The ^%x+$ guard closes the signed-hex hole, and the per-op verdict cache resets when the binding changes.

Checked:

  • ./run_tests.sh clean on LuaJIT 2.1, Lua 5.5 and 5.1 (no binding).
  • Mutants, each fails the suite: ROUTE_MIN_RATIO 4 to 3, hex guard removed, require_nonzero made a no-op.
  • CI green at this head. The three openssl-matrix logs show accelerated=true for all five ops on 0.8.5, 0.9.2 and 0.11.1.

Once #19 lands this branch needs a rebase. That moves the head, so it will need a fresh approval.

@svc-finitelabs svc-finitelabs Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed the delta since b91226f: 7dc23f3 (comment and doc trim on this branch) and the merge of #19's branch (1db4d66). #19's own trim commit, da66617, is reviewed on #19.

  • 1db4d66 is a clean merge: its tree equals git merge-tree --write-tree 7dc23f3 da66617.
  • 7dc23f3 changes no code. With comment-only and blank lines removed, bignum.lua is identical before and after, and the comparison catches a one-character mutation (ROUTE_MIN_RATIO = 3). No block comments or luacheck:-style directives are touched. The build.yml, init.lua and openssl_wrapper.lua hunks change only comments.
  • The shortened comments still match the code. The routing test builds a fresh stand-in per shape, so "runs twice (check, then work)" holds. In the zero-divisor test, calls("mulmod") == 1 is the known-answer check alone. The CLAUDE.md and README rewrites keep the routing rule, the per-operation check and the narrow-mul JIT caveat.
  • run_tests.sh passes under LuaJIT 2.1 and Lua 5.5 at 1db4d66. CI is 12/12 green at this head.

@derek-miller
derek-miller enabled auto-merge (squash) October 18, 2026 01:12
@derek-miller
derek-miller merged commit cb1bc40 into main Oct 18, 2026
12 checks passed
@derek-miller
derek-miller deleted the claude/openssl-big-number-support-2a0c40 branch October 18, 2026 01:15
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