Skip to content

Commit 1079e57

Browse files
FiloSottilegopherbot
authored andcommitted
crypto/rsa,crypto/internal/bigmod: optimized short exponentiations
RSA encryption and verification performs an exponentiation by a value usually just a few bits long. The current strategy with table precomputation is not efficient. Add an ExpShort bigmod method, and use it in RSA public key operations. After this, almost all CPU time in encryption/verification is spent preparing the constants for the modulus, because PublicKey doesn't have a Precompute function. This speeds up signing a bit too, because it performs a verification to protect against faults. name old time/op new time/op delta DecryptPKCS1v15/2048-4 1.13ms ± 0% 1.13ms ± 0% -0.43% (p=0.000 n=8+9) DecryptPKCS1v15/3072-4 3.20ms ± 0% 3.15ms ± 0% -1.59% (p=0.000 n=10+8) DecryptPKCS1v15/4096-4 6.45ms ± 0% 6.42ms ± 0% -0.49% (p=0.000 n=10+10) EncryptPKCS1v15/2048-4 132µs ± 0% 108µs ± 0% -17.99% (p=0.000 n=10+10) DecryptOAEP/2048-4 1.13ms ± 0% 1.14ms ± 0% +0.91% (p=0.000 n=10+10) EncryptOAEP/2048-4 132µs ± 0% 108µs ± 0% -18.09% (p=0.000 n=10+10) SignPKCS1v15/2048-4 1.18ms ± 0% 1.14ms ± 1% -3.30% (p=0.000 n=10+10) VerifyPKCS1v15/2048-4 131µs ± 0% 107µs ± 0% -18.30% (p=0.000 n=9+10) SignPSS/2048-4 1.18ms ± 0% 1.15ms ± 1% -1.87% (p=0.000 n=10+10) VerifyPSS/2048-4 132µs ± 0% 108µs ± 0% -18.30% (p=0.000 n=10+9) Updates #57752 Change-Id: Ic89273a58002b32b1c5c3185a35262694ceef409 Reviewed-on: https://go-review.googlesource.com/c/go/+/492935 Run-TryBot: Filippo Valsorda <[email protected]> Auto-Submit: Filippo Valsorda <[email protected]> Reviewed-by: Roland Shoemaker <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Reviewed-by: Matthew Dempsky <[email protected]>
1 parent 7d96475 commit 1079e57

File tree

3 files changed

+57
-32
lines changed

3 files changed

+57
-32
lines changed

src/crypto/internal/bigmod/nat.go

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -522,7 +522,7 @@ func (x *Nat) Add(y *Nat, m *Modulus) *Nat {
522522
func (x *Nat) montgomeryRepresentation(m *Modulus) *Nat {
523523
// A Montgomery multiplication (which computes a * b / R) by R * R works out
524524
// to a multiplication by R, which takes the value out of the Montgomery domain.
525-
return x.montgomeryMul(NewNat().set(x), m.rr, m)
525+
return x.montgomeryMul(x, m.rr, m)
526526
}
527527

528528
// montgomeryReduction calculates x = x / R mod m, with R = 2^(_W * n) and
@@ -533,10 +533,9 @@ func (x *Nat) montgomeryReduction(m *Modulus) *Nat {
533533
// By Montgomery multiplying with 1 not in Montgomery representation, we
534534
// convert out back from Montgomery representation, because it works out to
535535
// dividing by R.
536-
t0 := NewNat().set(x)
537-
t1 := NewNat().ExpandFor(m)
538-
t1.limbs[0] = 1
539-
return x.montgomeryMul(t0, t1, m)
536+
one := NewNat().ExpandFor(m)
537+
one.limbs[0] = 1
538+
return x.montgomeryMul(x, one, m)
540539
}
541540

542541
// montgomeryMul calculates x = a * b / R mod m, with R = 2^(_W * n) and
@@ -681,10 +680,10 @@ func addMulVVW(z, x []uint, y uint) (carry uint) {
681680
return carry
682681
}
683682

684-
// Mul calculates x *= y mod m.
683+
// Mul calculates x = x * y mod m.
685684
//
686-
// x and y must already be reduced modulo m, they must share its announced
687-
// length, and they may not alias.
685+
// The length of both operands must be the same as the modulus. Both operands
686+
// must already be reduced modulo m.
688687
func (x *Nat) Mul(y *Nat, m *Modulus) *Nat {
689688
// A Montgomery multiplication by a value out of the Montgomery domain
690689
// takes the result out of Montgomery representation.
@@ -716,28 +715,51 @@ func (out *Nat) Exp(x *Nat, e []byte, m *Modulus) *Nat {
716715
out.resetFor(m)
717716
out.limbs[0] = 1
718717
out.montgomeryRepresentation(m)
719-
t0 := NewNat().ExpandFor(m)
720-
t1 := NewNat().ExpandFor(m)
718+
tmp := NewNat().ExpandFor(m)
721719
for _, b := range e {
722720
for _, j := range []int{4, 0} {
723721
// Square four times. Optimization note: this can be implemented
724722
// more efficiently than with generic Montgomery multiplication.
725-
t1.montgomeryMul(out, out, m)
726-
out.montgomeryMul(t1, t1, m)
727-
t1.montgomeryMul(out, out, m)
728-
out.montgomeryMul(t1, t1, m)
723+
out.montgomeryMul(out, out, m)
724+
out.montgomeryMul(out, out, m)
725+
out.montgomeryMul(out, out, m)
726+
out.montgomeryMul(out, out, m)
729727

730728
// Select x^k in constant time from the table.
731729
k := uint((b >> j) & 0b1111)
732730
for i := range table {
733-
t0.assign(ctEq(k, uint(i+1)), table[i])
731+
tmp.assign(ctEq(k, uint(i+1)), table[i])
734732
}
735733

736734
// Multiply by x^k, discarding the result if k = 0.
737-
t1.montgomeryMul(out, t0, m)
738-
out.assign(not(ctEq(k, 0)), t1)
735+
tmp.montgomeryMul(out, tmp, m)
736+
out.assign(not(ctEq(k, 0)), tmp)
739737
}
740738
}
741739

742740
return out.montgomeryReduction(m)
743741
}
742+
743+
// ExpShort calculates out = x^e mod m.
744+
//
745+
// The output will be resized to the size of m and overwritten. x must already
746+
// be reduced modulo m. This leaks the exact bit size of the exponent.
747+
func (out *Nat) ExpShort(x *Nat, e uint, m *Modulus) *Nat {
748+
xR := NewNat().set(x).montgomeryRepresentation(m)
749+
750+
out.resetFor(m)
751+
out.limbs[0] = 1
752+
out.montgomeryRepresentation(m)
753+
754+
// For short exponents, precomputing a table and using a window like in Exp
755+
// doesn't pay off. Instead, we do a simple constant-time conditional
756+
// square-and-multiply chain, skipping the initial run of zeroes.
757+
tmp := NewNat().ExpandFor(m)
758+
for i := bits.UintSize - bitLen(e); i < bits.UintSize; i++ {
759+
out.montgomeryMul(out, out, m)
760+
k := (e >> (bits.UintSize - i - 1)) & 1
761+
tmp.montgomeryMul(out, xR, m)
762+
out.assign(ctEq(k, 1), tmp)
763+
}
764+
return out.montgomeryReduction(m)
765+
}

src/crypto/internal/bigmod/nat_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,17 @@ func TestExp(t *testing.T) {
299299
}
300300
}
301301

302+
func TestExpShort(t *testing.T) {
303+
m := modulusFromBytes([]byte{13})
304+
x := &Nat{[]uint{3}}
305+
out := &Nat{[]uint{0}}
306+
out.ExpShort(x, 12, m)
307+
expected := &Nat{[]uint{1}}
308+
if out.Equal(expected) != 1 {
309+
t.Errorf("%+v != %+v", out, expected)
310+
}
311+
}
312+
302313
// TestMulReductions tests that Mul reduces results equal or slightly greater
303314
// than the modulus. Some Montgomery algorithms don't and need extra care to
304315
// return correct results. See https://go.dev/issue/13907.

src/crypto/rsa/rsa.go

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ import (
3333
"crypto/internal/randutil"
3434
"crypto/rand"
3535
"crypto/subtle"
36-
"encoding/binary"
3736
"errors"
3837
"hash"
3938
"io"
@@ -462,25 +461,18 @@ var ErrMessageTooLong = errors.New("crypto/rsa: message too long for RSA key siz
462461
func encrypt(pub *PublicKey, plaintext []byte) ([]byte, error) {
463462
boring.Unreachable()
464463

464+
// Most of the CPU time for encryption and verification is spent in this
465+
// NewModulusFromBig call, because PublicKey doesn't have a Precomputed
466+
// field. If performance becomes an issue, consider placing a private
467+
// sync.Once on PublicKey to compute this.
465468
N := bigmod.NewModulusFromBig(pub.N)
466469
m, err := bigmod.NewNat().SetBytes(plaintext, N)
467470
if err != nil {
468471
return nil, err
469472
}
470-
e := intToBytes(pub.E)
473+
e := uint(pub.E)
471474

472-
return bigmod.NewNat().Exp(m, e, N).Bytes(N), nil
473-
}
474-
475-
// intToBytes returns i as a big-endian slice of bytes with no leading zeroes,
476-
// leaking only the bit size of i through timing side-channels.
477-
func intToBytes(i int) []byte {
478-
b := make([]byte, 8)
479-
binary.BigEndian.PutUint64(b, uint64(i))
480-
for len(b) > 1 && b[0] == 0 {
481-
b = b[1:]
482-
}
483-
return b
475+
return bigmod.NewNat().ExpShort(m, e, N).Bytes(N), nil
484476
}
485477

486478
// EncryptOAEP encrypts the given message with RSA-OAEP.
@@ -648,7 +640,7 @@ func decrypt(priv *PrivateKey, ciphertext []byte, check bool) ([]byte, error) {
648640
}
649641

650642
if check {
651-
c1 := bigmod.NewNat().Exp(m, intToBytes(priv.E), N)
643+
c1 := bigmod.NewNat().ExpShort(m, uint(priv.E), N)
652644
if c1.Equal(c) != 1 {
653645
return nil, ErrDecryption
654646
}

0 commit comments

Comments
 (0)