Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion crypto/internal/fips140/rsa/cast.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package rsa
import (
"bytes"
"errors"
"math/big"
"sync"

"github.com/runZeroInc/excrypto/crypto/internal/fips140"
Expand Down Expand Up @@ -169,7 +170,7 @@ func testPrivateKey() *PrivateKey {
0xA7, 0x50, 0x6D, 0xEB, 0x52, 0x39, 0xA8, 0xA7}
return &PrivateKey{
pub: PublicKey{
N: N, E: 65537,
N: N, E: big.NewInt(65537),
},
d: d, p: p, q: q, qInv: qInv, dP: dP, dQ: dQ,
fipsApproved: true,
Expand Down
3 changes: 2 additions & 1 deletion crypto/internal/fips140/rsa/keygen.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package rsa
import (
"errors"
"io"
"math/big"

"github.com/runZeroInc/excrypto/crypto/internal/fips140"
"github.com/runZeroInc/excrypto/crypto/internal/fips140/bigmod"
Expand Down Expand Up @@ -106,7 +107,7 @@ func GenerateKey(rand io.Reader, bits int) (*PrivateKey, error) {
// negligible chance of failure we can defer the check to the end of key
// generation and return an error if it fails. See [checkPrivateKey].

k, err := newPrivateKey(N, 65537, d, P, Q)
k, err := newPrivateKey(N, big.NewInt(65537), d, P, Q)
if err != nil {
return nil, err
}
Expand Down
75 changes: 52 additions & 23 deletions crypto/internal/fips140/rsa/rsa.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ package rsa
import (
"bytes"
"errors"
"math/big"

"github.com/runZeroInc/excrypto/crypto/internal/fips140"
"github.com/runZeroInc/excrypto/crypto/internal/fips140/bigmod"
)

type PublicKey struct {
N *bigmod.Modulus
E int
E *big.Int
}

// Size returns the modulus size in bytes. Raw signatures and ciphertexts
Expand Down Expand Up @@ -50,7 +51,10 @@ func (priv *PrivateKey) PublicKey() *PublicKey {
//
// All values are in big-endian byte slice format, and may have leading zeros
// or be shorter if leading zeroes were trimmed.
func NewPrivateKey(N []byte, e int, d, P, Q []byte) (*PrivateKey, error) {
func NewPrivateKey(N []byte, e *big.Int, d, P, Q []byte) (*PrivateKey, error) {
if e == nil {
return nil, errors.New("crypto/rsa: missing public exponent")
}
n, err := bigmod.NewModulus(N)
if err != nil {
return nil, err
Expand All @@ -67,10 +71,11 @@ func NewPrivateKey(N []byte, e int, d, P, Q []byte) (*PrivateKey, error) {
if err != nil {
return nil, err
}
return newPrivateKey(n, e, dN, p, q)
// Defensively copy e so callers cannot mutate the stored exponent.
return newPrivateKey(n, new(big.Int).Set(e), dN, p, q)
}

func newPrivateKey(n *bigmod.Modulus, e int, d *bigmod.Nat, p, q *bigmod.Modulus) (*PrivateKey, error) {
func newPrivateKey(n *bigmod.Modulus, e *big.Int, d *bigmod.Nat, p, q *bigmod.Modulus) (*PrivateKey, error) {
Comment thread
hdm marked this conversation as resolved.
pMinusOne := p.Nat().SubOne(p)
pMinusOneMod, err := bigmod.NewModulus(pMinusOne.Bytes(p))
if err != nil {
Expand All @@ -95,8 +100,13 @@ func newPrivateKey(n *bigmod.Modulus, e int, d *bigmod.Nat, p, q *bigmod.Modulus
qInv := bigmod.NewNat().Mod(q.Nat(), p)
qInv.Exp(qInv, pMinusTwo, p)

if e == nil {
return nil, errors.New("crypto/rsa: missing public exponent")
}
pk := &PrivateKey{
pub: PublicKey{
// e is already a defensive copy from NewPrivateKey or a freshly
// generated value from keygen; store directly.
N: n, E: e,
},
d: d, p: p, q: q,
Expand All @@ -110,7 +120,7 @@ func newPrivateKey(n *bigmod.Modulus, e int, d *bigmod.Nat, p, q *bigmod.Modulus

// NewPrivateKeyWithPrecomputation creates a new RSA private key from the given
// parameters, which include precomputed CRT values.
func NewPrivateKeyWithPrecomputation(N []byte, e int, d, P, Q, dP, dQ, qInv []byte) (*PrivateKey, error) {
func NewPrivateKeyWithPrecomputation(N []byte, e *big.Int, d, P, Q, dP, dQ, qInv []byte) (*PrivateKey, error) {
Comment thread
hdm marked this conversation as resolved.
n, err := bigmod.NewModulus(N)
if err != nil {
return nil, err
Expand All @@ -132,9 +142,13 @@ func NewPrivateKeyWithPrecomputation(N []byte, e int, d, P, Q, dP, dQ, qInv []by
return nil, err
}

if e == nil {
return nil, errors.New("crypto/rsa: missing public exponent")
}
pk := &PrivateKey{
pub: PublicKey{
N: n, E: e,
// Defensively copy e so callers cannot mutate the stored exponent.
N: n, E: new(big.Int).Set(e),
},
d: dN, p: p, q: q,
dP: dP, dQ: dQ, qInv: qInvNat,
Expand All @@ -148,7 +162,7 @@ func NewPrivateKeyWithPrecomputation(N []byte, e int, d, P, Q, dP, dQ, qInv []by
// NewPrivateKeyWithoutCRT creates a new RSA private key from the given parameters.
//
// This is meant for deprecated multi-prime keys, and is not FIPS 140 compliant.
func NewPrivateKeyWithoutCRT(N []byte, e int, d []byte) (*PrivateKey, error) {
func NewPrivateKeyWithoutCRT(N []byte, e *big.Int, d []byte) (*PrivateKey, error) {
n, err := bigmod.NewModulus(N)
if err != nil {
return nil, err
Expand All @@ -157,9 +171,13 @@ func NewPrivateKeyWithoutCRT(N []byte, e int, d []byte) (*PrivateKey, error) {
if err != nil {
return nil, err
}
if e == nil {
return nil, errors.New("crypto/rsa: missing public exponent")
}
pk := &PrivateKey{
pub: PublicKey{
N: n, E: e,
// Defensively copy e so callers cannot mutate the stored exponent.
N: n, E: new(big.Int).Set(e),
},
d: dN,
}
Expand All @@ -173,9 +191,10 @@ func NewPrivateKeyWithoutCRT(N []byte, e int, d []byte) (*PrivateKey, error) {
//
// P, Q, dP, dQ, and qInv may be nil if the key was created with
// NewPrivateKeyWithoutCRT.
func (priv *PrivateKey) Export() (N []byte, e int, d, P, Q, dP, dQ, qInv []byte) {
func (priv *PrivateKey) Export() (N []byte, e *big.Int, d, P, Q, dP, dQ, qInv []byte) {
N = priv.pub.N.Nat().Bytes(priv.pub.N)
e = priv.pub.E
// Defensively copy E so the caller cannot mutate internal state.
e = new(big.Int).Set(priv.pub.E)
d = priv.d.Bytes(priv.pub.N)
if priv.dP == nil {
return
Expand Down Expand Up @@ -244,7 +263,13 @@ func checkPrivateKey(priv *PrivateKey) error {
return errors.New("crypto/rsa: invalid CRT exponent")
}
de := bigmod.NewNat()
de.SetUint(uint(priv.pub.E)).ExpandFor(pMinus1)
// e may be larger than p-1 (the case for tiny test keys), so reduce
// modulo p-1 before loading into a bigmod.Nat sized for pMinus1.
ePmod := new(big.Int).Mod(priv.pub.E, new(big.Int).SetBytes(p.Nat().SubOne(p).Bytes(p)))
if _, err := de.SetBytes(ePmod.Bytes(), pMinus1); err != nil {
return errors.New("crypto/rsa: invalid public exponent")
}
de.ExpandFor(pMinus1)
de.Mul(dP, pMinus1)
if de.IsOne() != 1 {
return errors.New("crypto/rsa: invalid CRT exponent")
Expand All @@ -258,7 +283,11 @@ func checkPrivateKey(priv *PrivateKey) error {
if err != nil {
return errors.New("crypto/rsa: invalid CRT exponent")
}
de.SetUint(uint(priv.pub.E)).ExpandFor(qMinus1)
eQmod := new(big.Int).Mod(priv.pub.E, new(big.Int).SetBytes(q.Nat().SubOne(q).Bytes(q)))
if _, err := de.SetBytes(eQmod.Bytes(), qMinus1); err != nil {
return errors.New("crypto/rsa: invalid public exponent")
}
de.ExpandFor(qMinus1)
de.Mul(dQ, qMinus1)
if de.IsOne() != 1 {
return errors.New("crypto/rsa: invalid CRT exponent")
Expand Down Expand Up @@ -341,25 +370,25 @@ func checkPublicKey(pub *PublicKey) (fipsApproved bool, err error) {
if pub.N.BitLen()%2 == 1 {
fipsApproved = false
}
if pub.E < 2 {
if pub.E == nil || pub.E.Sign() <= 0 || pub.E.Cmp(big.NewInt(2)) < 0 {
return false, errors.New("crypto/rsa: public exponent too small or negative")
}
// e needs to be coprime with p-1 and q-1, since it must be invertible
// modulo λ(pq). Since p and q are prime, this means e needs to be odd.
if pub.E&1 == 0 {
if pub.E.Bit(0) == 0 {
return false, errors.New("crypto/rsa: public exponent is even")
}
// FIPS 186-5, Section 5.5(e): "The exponent e shall be an odd, positive
// integer such that 2¹⁶ < e < 2²⁵⁶."
if pub.E <= 1<<16 {
if pub.E.Cmp(big.NewInt(1<<16)) <= 0 {
fipsApproved = false
}
// We require pub.E to fit into a 32-bit integer so that we
// do not have different behavior depending on whether
// int is 32 or 64 bits. See also
// https://www.imperialviolet.org/2012/03/16/rsae.html.
if pub.E > 1<<31-1 {
return false, errors.New("crypto/rsa: public exponent too large")
// Previously FIPS required pub.E to fit in a 32-bit integer; excrypto
// relaxes this so that pathological public keys observed in the wild
// can still be parsed and inspected. Operations that depend on a small
// e (FIPS mode) are still gated above via fipsApproved.
if pub.E.BitLen() > 31 {
fipsApproved = false
Comment thread
hdm marked this conversation as resolved.
}
return fipsApproved, nil
}
Expand All @@ -378,7 +407,7 @@ func encrypt(pub *PublicKey, plaintext []byte) ([]byte, error) {
if err != nil {
return nil, err
}
return bigmod.NewNat().ExpShortVarTime(m, uint(pub.E), pub.N).Bytes(pub.N), nil
return bigmod.NewNat().Exp(m, pub.E.Bytes(), pub.N).Bytes(pub.N), nil
}

var ErrMessageTooLong = errors.New("crypto/rsa: message too long for RSA key size")
Expand Down Expand Up @@ -440,7 +469,7 @@ func decrypt(priv *PrivateKey, ciphertext []byte, check bool) ([]byte, error) {
}

if check {
c1 := bigmod.NewNat().ExpShortVarTime(m, uint(E), N)
c1 := bigmod.NewNat().Exp(m, E.Bytes(), N)
if c1.Equal(c) != 1 {
return nil, ErrDecryption
}
Expand Down
10 changes: 5 additions & 5 deletions crypto/internal/fips140test/acvp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1895,7 +1895,7 @@ func cmdRsaKeyGenAft() command {
N, e, d, P, Q, _, _, _ := key.Export()

eBytes := make([]byte, 4)
binary.BigEndian.PutUint32(eBytes, uint32(e))
binary.BigEndian.PutUint32(eBytes, uint32(e.Uint64()))

return [][]byte{eBytes, P, Q, N, d}, nil
},
Expand Down Expand Up @@ -1933,7 +1933,7 @@ func cmdRsaSigGenAft(hashFunc func() hash.Hash, hashName string, pss bool) comma

N, e, _, _, _, _, _, _ := key.Export()
eBytes := make([]byte, 4)
binary.BigEndian.PutUint32(eBytes, uint32(e))
binary.BigEndian.PutUint32(eBytes, uint32(e.Uint64()))

return [][]byte{N, eBytes, sig}, nil
},
Expand All @@ -1960,7 +1960,7 @@ func cmdRsaSigVerAft(hashFunc func() hash.Hash, hashName string, pss bool) comma

pub := &rsa.PublicKey{
N: n,
E: e,
E: big.NewInt(int64(e)),
}

h := hashFunc()
Expand Down Expand Up @@ -2050,7 +2050,7 @@ func cmdKtsIfcInitiatorAft(h func() hash.Hash) command {

pub := &rsa.PublicKey{
N: n,
E: e,
E: big.NewInt(int64(e)),
}

dkm := make([]byte, outputBytes)
Expand Down Expand Up @@ -2088,7 +2088,7 @@ func cmdKtsIfcResponderAft(h func() hash.Hash) command {
return nil, errors.New("e must be 0x10001")
}

priv, err := rsa.NewPrivateKey(nBytes, int(e), dBytes, pBytes, qBytes)
priv, err := rsa.NewPrivateKey(nBytes, big.NewInt(int64(e)), dBytes, pBytes, qBytes)
if err != nil {
return nil, fmt.Errorf("failed to create private key: %v", err)
}
Expand Down
17 changes: 12 additions & 5 deletions crypto/json/rsa.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ type RSAPublicKey struct {
}

type auxRSAPublicKey struct {
Exponent int `json:"exponent"`
Modulus []byte `json:"modulus"`
Length int `json:"length"`
Exponent json.Number `json:"exponent"`
Modulus []byte `json:"modulus"`
Length int `json:"length"`
}

// RSAClientParams are the TLS key exchange parameters for RSA keys.
Expand All @@ -43,7 +43,9 @@ type RSAClientParams struct {
func (rp *RSAPublicKey) MarshalJSON() ([]byte, error) {
var aux auxRSAPublicKey
if rp.PublicKey != nil {
aux.Exponent = rp.E
if rp.E != nil {
aux.Exponent = json.Number(rp.E.String())
}
aux.Modulus = rp.N.Bytes()
aux.Length = len(aux.Modulus) * 8
}
Expand All @@ -59,7 +61,12 @@ func (rp *RSAPublicKey) UnmarshalJSON(b []byte) error {
if rp.PublicKey == nil {
rp.PublicKey = new(rsa.PublicKey)
}
rp.E = aux.Exponent
rp.E = new(big.Int)
if s := string(aux.Exponent); s != "" {
if _, ok := rp.E.SetString(s, 10); !ok {
return fmt.Errorf("invalid RSA public exponent %q", s)
}
}
rp.N = big.NewInt(0).SetBytes(aux.Modulus)
if len(aux.Modulus)*8 != aux.Length {
return fmt.Errorf("mismatched length (got %d, field specified %d)", len(aux.Modulus), aux.Length)
Expand Down
2 changes: 1 addition & 1 deletion crypto/json/rsa_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ var _ = Suite(&RSASuite{})
func (s *RSASuite) SetUpTest(c *C) {
s.pk4096 = new(RSAPublicKey)
s.pk4096.PublicKey = new(rsa.PublicKey)
s.pk4096.E = 65537
s.pk4096.E = big.NewInt(65537)
s.pk4096.N = big.NewInt(0).SetBytes(test4096Modulus)
}

Expand Down
56 changes: 56 additions & 0 deletions crypto/rsa/bench_e_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright 2026 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package rsa_test

import (
"crypto/rand"
"math/big"
"testing"

. "github.com/runZeroInc/excrypto/crypto/rsa"
)

// BenchmarkVerifyByExponentBitLen measures the cost of a single PKCS#1 v1.5
// signature verification on a 2048-bit RSA modulus as E grows. The verifier
// performs m^e mod n; modular exponentiation is O(bitlen(e) · bitlen(n)²).
func BenchmarkVerifyByExponentBitLen(b *testing.B) {
priv, err := GenerateKey(rand.Reader, 2048)
if err != nil {
b.Fatal(err)
}
// Build a synthetic odd E of the requested bit length, coprime to phi(n)
// is not required here: we measure CPU of the exponentiation itself, and
// any odd E exercises the same code path.
makeE := func(bits int) *big.Int {
e := new(big.Int).Lsh(big.NewInt(1), uint(bits-1))
e.SetBit(e, 0, 1)
return e
}
for _, bits := range []int{17, 64, 128, 256, 512, 1024, 2048} {
b.Run("E="+itoa(bits)+"b", func(b *testing.B) {
pub := &PublicKey{N: priv.N, E: makeE(bits)}
msg := []byte("x")
// Use Encrypt as a proxy for the public-key operation.
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = EncryptPKCS1v15(rand.Reader, pub, msg)
}
})
}
}

func itoa(n int) string {
if n == 0 {
return "0"
}
var b [20]byte
i := len(b)
for n > 0 {
i--
b[i] = byte('0' + n%10)
n /= 10
}
return string(b[i:])
}
4 changes: 2 additions & 2 deletions crypto/rsa/boring_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func TestBoringVerify(t *testing.T) {
// Check that signatures that lack leading zeroes don't verify.
key := &PublicKey{
N: bigFromHex("c4fdf7b40a5477f206e6ee278eaef888ca73bf9128a9eef9f2f1ddb8b7b71a4c07cfa241f028a04edb405e4d916c61d6beabc333813dc7b484d2b3c52ee233c6a79b1eea4e9cc51596ba9cd5ac5aeb9df62d86ea051055b79d03f8a4fa9f38386f5bd17529138f3325d46801514ea9047977e0829ed728e68636802796801be1"),
E: 65537,
E: big.NewInt(65537),
}

hash := fromHex("019c5571724fb5d0e47a4260c940e9803ba05a44")
Expand All @@ -63,7 +63,7 @@ func BenchmarkBoringVerify(b *testing.B) {
// Check that signatures that lack leading zeroes don't verify.
key := &PublicKey{
N: bigFromHex("c4fdf7b40a5477f206e6ee278eaef888ca73bf9128a9eef9f2f1ddb8b7b71a4c07cfa241f028a04edb405e4d916c61d6beabc333813dc7b484d2b3c52ee233c6a79b1eea4e9cc51596ba9cd5ac5aeb9df62d86ea051055b79d03f8a4fa9f38386f5bd17529138f3325d46801514ea9047977e0829ed728e68636802796801be1"),
E: 65537,
E: big.NewInt(65537),
}

hash := fromHex("019c5571724fb5d0e47a4260c940e9803ba05a44")
Expand Down
Loading
Loading