Repository navigation
rsa: sign and decrypt through the CRT form of the key - #59
Merged
Merged
Conversation
The parser read p, q, dP, dQ and qInv and dropped them, so every private-key operation was one full-size exponentiation. Keep them and use two half-size ones instead, with each result checked against e before it is returned. A 2048-bit TLS handshake goes from 13.7 to 2.6 ms of server CPU. Keys without usable CRT values fall back to d, which is now passed at the modulus's length rather than at the capacity of Modulus(4096).
nevindra
marked this pull request as ready for review
September 23, 2026 00:46
nevindra
added a commit
to nevindra/nilo
that referenced
this pull request
Sep 23, 2026
tls.zig signed with the full-size exponent and dropped the CRT values it parsed, so an RSA-2048 handshake cost 13.7 ms of server CPU. The pin moves to a fork carrying the fix (ianic/tls.zig#59) until upstream merges it: 2.6 ms a handshake, and the arena's 8gbit shape goes from a 1 s p99 to 29 ms.
This was referenced Oct 8, 2026
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.
An RSA private-key operation is currently one full-size exponentiation,
pow(m, d)onff.Modulus(4096). The parser reads the key's primes, their exponents and the coefficient, then discards them (_ = .{ exp1, exp2, coeff };). For a server with an RSA certificate, that signature is most of every TLS handshake.This PR keeps them and uses the Chinese Remainder Theorem (RFC 8017, section 5.1.2, step 2.b): two exponentiations with half-size moduli and half-size exponents, joined with
qInv.What changes
SecretKeygets an optionalcrt: ?Crt, filled by bothKeyPair.fromDer(PKCS coding style not following zig guideline #1) andPrivateKey.parseDer(PKCS Accept the async handler type as-is. #8). It defaults tonull, so existing struct literals and every caller keep compiling.Crt.initchecksn = p * qandq * qInv = 1 (mod p)at load. If the values are missing or do not describe the key, it returnsnulland the key signs withdas before. No key that loads today stops loading.encrypt, which both signers use, plusdecryptPkcsv1_5anddecryptOaep) go through oneKeyPair.powSecret.d.dfallback is faster too. It now passesdat the modulus's byte length throughpowWithEncodedExponent.Modulus.powencodes the exponent at the capacity of the type (512 bytes forModulus(4096)), so a 2048-bit key'sdwas half leading zeros, and the exponentiation worked through all of them. The CRT exponents are passed the same way. That is also why oneModulus(2048)type serves the primes of every key size: a 1024-bit prime in it costs the same as inModulus(1024).Numbers
Ryzen 7 9700X, Zig 0.16.0,
ReleaseFast,-Dcpu=x86_64_v3+aes+pclmul. The server is a TLS 1.3 HTTP server on this library (nilo), 8 threads pinned to 4 physical cores. The client is pinned to the other 4. The certificate is RSA-2048. Before and after were interleaved, two rounds each, and the rounds agreed to the digit shown.The last row is the handshakes alone: 4,096 of them at 13.7 ms each is more CPU than 8 threads have in 5 s.
Where the time goes, one 2048-bit private-key operation, single thread:
powonModulus(4096)dat the modulus's lengthdP/dQat their prime's lengthspeed rsa2048sign, for scaleWhat is left against OpenSSL is
std.crypto.ff's exponentiation itself, which this PR does not touch.Tests
zig build test: 91 passed, 1 skipped, in Debug, ReleaseSafe and ReleaseFast. Three tests are new:pow(x, d), and PKCS1-v1_5 signatures from both paths are byte-identical;dPstill gives a valid signature, because the fault check falls back tod;Crt.initrefuses a prime that does not dividen, and a coefficient that is notq^-1 mod p.zig build integration: 7 of 7 in Debug and ReleaseSafe, which includes client auth with RSA 2048, 3072 and 4096 keys.Binary size of a server built on it goes up by 35 KB, the second
ff.Modulusinstantiation. Memory per idle connection is unchanged (9,411 bytes at 2,000 connections, before and after).main
The same patch applies to
maincleanly (git apply --check), but I have not built it there, becausemainneeds Zig 0.17-dev. Happy to open a second PR againstmainif you want it there too.