Feature: Pull RSA large E support from zcrypto/zgrab2#78
Merged
Conversation
There was a problem hiding this comment.
Pull request overview
This PR changes RSA public exponents from fixed-width int values to *big.Int across RSA, X.509, SSH, OpenPGP, TLS, JSON, and FIPS-related code paths to support unusually large RSA exponents.
Changes:
- Converts RSA exponent parsing, marshaling, comparisons, and tests to use
*big.Int. - Relaxes several previous exponent-size rejection paths.
- Updates FIPS/internal RSA plumbing and ACVP test adapters for big integer exponents.
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
crypto/rsa/rsa.go |
Changes rsa.PublicKey.E to *big.Int and updates key generation/equality/precompute paths. |
crypto/rsa/fips.go |
Updates FIPS-only exponent checks for *big.Int. |
crypto/rsa/rsa_test.go |
Updates RSA tests for pointer exponent values. |
crypto/rsa/pss_test.go |
Updates PSS vector exponent parsing. |
crypto/rsa/boring_test.go |
Updates BoringCrypto RSA test keys. |
crypto/internal/fips140/rsa/rsa.go |
Converts internal FIPS RSA exponent handling to *big.Int. |
crypto/internal/fips140/rsa/keygen.go |
Uses big.NewInt for generated FIPS RSA exponent. |
crypto/internal/fips140/rsa/cast.go |
Updates test FIPS RSA key exponent initialization. |
crypto/internal/fips140test/acvp_test.go |
Adapts ACVP RSA exponent inputs/outputs. |
crypto/x509/pkcs1.go |
Updates PKCS#1 RSA public/private exponent ASN.1 structures and validation. |
crypto/x509/parser.go |
Parses X.509 RSA public exponents into big.Int. |
crypto/x509/x509_test.go |
Updates X.509 RSA tests and large-exponent expectations. |
crypto/ssl3/tls/key_agreement.go |
Serializes/parses SSL3/TLS export RSA exponents as big.Int. |
crypto/ssl3/tls/handshake_server_test.go |
Updates TLS test RSA key exponent. |
crypto/json/rsa.go |
Encodes/decodes RSA JSON exponent as json.Number/big.Int. |
crypto/json/rsa_test.go |
Updates JSON RSA test key exponent. |
x/crypto/ssh/keys.go |
Updates SSH RSA public/private key parsing and marshaling. |
x/crypto/ssh/agent/server.go |
Updates SSH agent RSA key/cert parsing. |
x/crypto/ssh/agent/client.go |
Sends RSA agent exponent as big.Int. |
x/crypto/openpgp/packet/public_key.go |
Updates OpenPGP RSA exponent construction/parsing. |
x/crypto/openpgp/packet/public_key_v3.go |
Updates V3 OpenPGP RSA exponent construction/parsing. |
x/crypto/openpgp/packet/encrypted_key_test.go |
Updates OpenPGP test RSA key exponent. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…y audit Address PR review concerns on large-E support: Safety - defensive copies of E - crypto/internal/fips140/rsa: NewPrivateKey, NewPrivateKeyWithPrecomputation and NewPrivateKeyWithoutCRT defensively copy the caller's *big.Int E into internal state. Export() also returns a defensive copy so callers cannot mutate the stored exponent. - crypto/rsa: fipsPublicKey copies pub.E into the FIPS PublicKey. - x/crypto/ssh: parseRSA copies wire-parsed E (and N) into the returned key. - x/crypto/ssh/agent: parseRSAKey copies E into the rsa.PrivateKey. Broad compatibility - removed accidental filters - Parse paths (crypto/x509, crypto/rsa, crypto/json, openpgp, ssh, ssh/agent) only reject when Sign() <= 0 (or, for SSH, also reject even or e<3, which RFC 4253 requires for correctness). No remaining size ceilings. - fips140/rsa.checkPublicKey: large E only flips fipsApproved=false; it no longer rejects the key. - crypto/x509 TestMarshalRSAPublicKey case #5 updated to expect success. DoS bound - configurable upper bound on E - New rsa.MaxPublicExponentBitLen (default 1024) gates Encrypt/Verify in the central checkPublicKeySize choke point. Parsing is never gated, so pathological keys can always be inspected. Setting the variable to 0 removes the bound entirely. - New benchmark crypto/rsa/bench_e_test.go and new tests TestMaxPublicExponentBitLen and TestPublicKeyDefensiveCopy in crypto/rsa/rsa_test.go. Per-op cost on Apple M3 Max, RSA-2048: bitlen(E) = 17 ~56 us (e = 65537) bitlen(E) = 256 ~340 us (FIPS 186-5 ceiling) bitlen(E) = 1024 ~1.25 ms (default) bitlen(E) = 2048 ~2.5 ms (E as wide as N; absolute worst case)
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.
Feature: Pull RSA large E support from zcrypto/zgrab2
Closes the long-standing gap where excrypto rejected RSA public exponents that
exceed
1 << 31 - 1and therefore could not parse or inspect a meaningfulslice of pathological-but-real keys observed in the wild by zmap/zgrab2 and
runZero. Mirrors the approach pioneered in
zcrypto and the SSH-specific fixes from
zgrab2#714, then extends them.
Summary of changes
1.
rsa.PublicKey.Eis now*big.IntThe hard 31-bit ceiling on
Eis gone.crypto/rsa.PublicKey.Eis a*big.Int, and every internal site that previously typed it asinthas beenupdated:
crypto/internal/fips140/rsa(NewPrivateKey,NewPrivateKeyWithPrecomputation,NewPrivateKeyWithoutCRT,Export,checkPublicKey,checkPrivateKey,encrypt,decrypt,cast.go,keygen.go)crypto/internal/fips140test/acvp_test.gocrypto/rsa(rsa.go,fips.go,boring_test.go,pss_test.go,rsa_test.go)crypto/json(rsa.go,rsa_test.go)crypto/x509(pkcs1.go,parser.go,x509_test.go)crypto/ssl3/tls(key_agreement.go,handshake_server_test.go)x/crypto/openpgp/packet(public_key.go,public_key_v3.go,encrypted_key_test.go)x/crypto/ssh(keys.go)x/crypto/ssh/agent(server.go,client.go)ASN.1 PKCS#1 structs (
pkcs1PrivateKey,pkcs1PublicKey) also use*big.Intso the wire encoding round-trips arbitrarily large exponents.crypto/jsonswitches the JSON exponent representation tojson.Numbertoencode/decode values that don't fit in a Go
int.2. Modular exponentiation no longer truncates
Ecrypto/internal/fips140/rsa.encrypt/decryptpreviously calledbigmod.Nat.ExpShortVarTime(x, uint(e), m), which silently capseat thesize of a
uint. They now usebigmod.Nat.Exp(x, e.Bytes(), m), which acceptsarbitrarily large exponents.
A tiny-key fix in
checkPrivateKey: whene > p-1(which happens fortiny test keys, never for real keys),
eis pre-reduced vianew(big.Int).Mod(e, p-1)before being loaded into abigmod.Natsized forpMinus1. The same is done forqMinus1. This fixesTestTinyKeyGeneration,TestEverything/32, andTestEverything/33.3. Removed all residual size filters on
EAudited every parse path. Only correctness checks remain — no size ceilings:
crypto/x509.ParsePKCS1PublicKey: only rejectspub.E.Sign() <= 0. The oldpub.E > 1<<31-1rejection is gone.TestMarshalRSAPublicKeycase zcrypto merge #5updated to expect success.
crypto/x509/parser.go(parsePublicKey): only rejectsSign() <= 0.x/crypto/ssh.parseRSA: droppedBitLen() > 24. Now rejects onlyE < 3 || E.Bit(0) == 0(required by RFC 4253 form^e mod nto beinvertible —
Emust be an odd integer ≥ 3).x/crypto/ssh/agent.parseRSAKey,parseRSACert: droppedBitLen() > 30;aligned with
parseRSA's odd-E ≥ 3requirement.crypto/internal/fips140/rsa.checkPublicKey: a largeEonly flipsfipsApproved = false. It no longer rejects the key.4. Defensive copies of
Eat every trust boundaryA reviewer-noted safety concern: storing the caller's
*big.IntEdirectlyinside long-lived state means the caller can mutate it later and corrupt
internal invariants. Every constructor that consumes a caller-supplied
Enow defensively copies it via
new(big.Int).Set(e):crypto/internal/fips140/rsa.NewPrivateKey,NewPrivateKeyWithPrecomputation,NewPrivateKeyWithoutCRTcrypto/internal/fips140/rsa.PrivateKey.Export(returns a copy so the callercannot mutate stored state through the returned value)
crypto/rsa.fipsPublicKeyx/crypto/ssh.parseRSA(copies bothEandNfrom the wire-parsed struct)x/crypto/ssh/agent.parseRSAKeyA new test
TestPublicKeyDefensiveCopy(crypto/rsa/rsa_test.go) verifiesthat mutating
priv.PublicKey.Eafter an operation does not break subsequentoperations on the same key.
5. Configurable DoS bound:
rsa.MaxPublicExponentBitLenAccepting unboundedly large
Efrom untrusted sources is a DoS vector:modular exponentiation is
O(bitlen(E) · bitlen(N)²). Indicative cost onApple M3 Max with a 2048-bit modulus:
bitlen(E)A new package variable,
rsa.MaxPublicExponentBitLen(default1024),gates the public-key operations (
EncryptPKCS1v15,EncryptOAEP,VerifyPKCS1v15,VerifyPSS, etc.) in the single choke pointcheckPublicKeySize. Parsing is never gated, so any key — no matter howpathological — can still be inspected. Users can tune the bound:
Tests:
TestMaxPublicExponentBitLenandBenchmarkVerifyByExponentBitLen.6. SSH parity with zgrab2#714
Beyond the
parseRSABitLen > 24removal already noted, the SSH agentside mirrors the PR:
x/crypto/ssh/agent/server.go:parseRSAKeyandparseRSACertno longercap
Eat 30 bits.x/crypto/ssh/agent/client.go: passesk.Edirectly (now*big.Int)instead of
big.NewInt(int64(k.E)).x/crypto/ssh/keys.go:Marshal,parseOpenSSHPrivateKey, andMarshalPrivateKeythreadEthrough as*big.Int.Differences from upstream zcrypto / zgrab2
This branch is a strict superset of the upstream patches plus several
additions that surfaced from PR review:
Eto*big.Intand otherwise leavescallers to defend themselves. This PR adds a configurable, documented
MaxPublicExponentBitLengate with a permissive default (1024) and concreteper-op cost numbers in the doc comment so callers can tune sensibly.
*big.Intdirectlyinto internal state. This PR copies at every boundary, and also copies on
the way out via
Export. NewTestPublicKeyDefensiveCopycovers this.bigmod.Nat.Expinstead ofExpShortVarTime. Required to actuallyevaluate operations with
bitlen(E) > 64; without this change, largeEparses but silently misbehaves on encrypt/verify.
checkPrivateKeypath now reducesEmodulop-1andq-1before loading intobigmod.Nat, so test keys withbitlen(N) < bitlen(E)validate correctly.crypto/jsonusesjson.Numberfor the exponent so values that exceed2^53survive JSON round-trips intact.x/crypto/ssh/agentparity. zgrab2#714 only touchesx/crypto/ssh;this PR extends the same treatment to
x/crypto/ssh/agent's server andclient, with the same
odd, E ≥ 3validation.Validation
go build ./...— cleango vet ./...— cleango test ./...— all packages pass except a pre-existing macOSssh-agentUnix-socket-path-length environment failure (verified toreproduce on
mainindependently of this branch).See also: zgrab2#714 (the SSH
piece) and zcrypto's
crypto/rsa.PublicKey.E *big.Intdesign.