crypto/rsa: check for post-Precompute changes in Validate

Updates #74115

Change-Id: I6a6a6964be55cff5131d99235f621b4ff2a93d2b
Reviewed-on: https://go-review.googlesource.com/c/go/+/687835
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Mark Freeman <markfreeman@google.com>
Reviewed-by: Michael Pratt <mpratt@google.com>
Reviewed-by: Daniel McCarney <daniel@binaryparadox.net>
Auto-Submit: Filippo Valsorda <filippo@golang.org>
This commit is contained in:
Filippo Valsorda 2025-07-10 17:46:48 +02:00 committed by Gopher Robot
parent 968a5107a9
commit 5d9d0513dc
4 changed files with 215 additions and 43 deletions

View file

@ -0,0 +1,2 @@
If [PrivateKey] fields are modified after calling [PrivateKey.Precompute],
[PrivateKey.Validate] now fails.

View file

@ -49,4 +49,10 @@ func TestEqual(t *testing.T) {
if private.Equal(other) {
t.Errorf("different private keys are Equal")
}
noPrecomp := *private
noPrecomp.Precomputed = rsa.PrecomputedValues{}
if !private.Equal(&noPrecomp) {
t.Errorf("private key with no precomputation is not equal to itself: %v", private)
}
}

View file

@ -103,7 +103,10 @@ type OAEPOptions struct {
Label []byte
}
// A PrivateKey represents an RSA key
// A PrivateKey represents an RSA key.
//
// Its fields must not be modified after calling [PrivateKey.Precompute], and
// should not be used directly as big.Int values for cryptographic purposes.
type PrivateKey struct {
PublicKey // public part.
D *big.Int // private exponent
@ -111,7 +114,7 @@ type PrivateKey struct {
// Precomputed contains precomputed values that speed up RSA operations,
// if available. It must be generated by calling PrivateKey.Precompute and
// must not be modified.
// must not be modified afterwards.
Precomputed PrecomputedValues
}
@ -228,21 +231,52 @@ type CRTValue struct {
//
// It runs faster on valid keys if run after [PrivateKey.Precompute].
func (priv *PrivateKey) Validate() error {
// We can operate on keys based on d alone, but it isn't possible to encode
// with [crypto/x509.MarshalPKCS1PrivateKey], which unfortunately doesn't
// return an error.
// We can operate on keys based on d alone, but they can't be encoded with
// [crypto/x509.MarshalPKCS1PrivateKey], which unfortunately doesn't return
// an error, so we need to reject them here.
if len(priv.Primes) < 2 {
return errors.New("crypto/rsa: missing primes")
}
// If Precomputed.fips is set, then the key has been validated by
// [rsa.NewPrivateKey] or [rsa.NewPrivateKeyWithoutCRT].
if priv.Precomputed.fips != nil {
// If Precomputed.fips is set and consistent, then the key has been
// validated by [rsa.NewPrivateKey] or [rsa.NewPrivateKeyWithoutCRT].
if priv.precomputedIsConsistent() {
return nil
}
if priv.Precomputed.fips != nil {
return errors.New("crypto/rsa: precomputed values are inconsistent with the key")
}
_, err := priv.precompute()
return err
}
func (priv *PrivateKey) precomputedIsConsistent() bool {
if priv.Precomputed.fips == nil {
return false
}
N, e, d, P, Q, dP, dQ, qInv := priv.Precomputed.fips.Export()
if !bigIntEqualToBytes(priv.N, N) || priv.E != e || !bigIntEqualToBytes(priv.D, d) {
return false
}
if len(priv.Primes) != 2 {
return P == nil && Q == nil && dP == nil && dQ == nil && qInv == nil
}
return bigIntEqualToBytes(priv.Primes[0], P) &&
bigIntEqualToBytes(priv.Primes[1], Q) &&
bigIntEqualToBytes(priv.Precomputed.Dp, dP) &&
bigIntEqualToBytes(priv.Precomputed.Dq, dQ) &&
bigIntEqualToBytes(priv.Precomputed.Qinv, qInv)
}
// bigIntEqual reports whether a and b are equal, ignoring leading zero bytes in
// b, and leaking only their bit length through timing side-channels.
func bigIntEqualToBytes(a *big.Int, b []byte) bool {
if a == nil || a.BitLen() > len(b)*8 {
return false
}
buf := a.FillBytes(make([]byte, len(b)))
return subtle.ConstantTimeCompare(buf, b) == 1
}
// rsa1024min is a GODEBUG that re-enables weak RSA keys if set to "0".
// See https://go.dev/issue/68762.
var rsa1024min = godebug.New("rsa1024min")
@ -505,14 +539,15 @@ var ErrVerification = errors.New("crypto/rsa: verification error")
// Precompute performs some calculations that speed up private key operations
// in the future. It is safe to run on non-validated private keys.
func (priv *PrivateKey) Precompute() {
if priv.Precomputed.fips != nil {
if priv.precomputedIsConsistent() {
return
}
precomputed, err := priv.precompute()
if err != nil {
// We don't have a way to report errors, so just leave the key
// unmodified. Validate will re-run precompute.
// We don't have a way to report errors, so just leave Precomputed.fips
// nil. Validate will re-run precompute and report its error.
priv.Precomputed.fips = nil
return
}
priv.Precomputed = precomputed

View file

@ -23,6 +23,7 @@ import (
"io"
"math/big"
"os"
"slices"
"strings"
"testing"
)
@ -226,8 +227,9 @@ func TestEverything(t *testing.T) {
}
func testEverything(t *testing.T, priv *PrivateKey) {
if err := priv.Validate(); err != nil {
t.Errorf("Validate() failed: %s", err)
validateErr := priv.Validate()
if validateErr != nil && len(priv.Primes) >= 2 {
t.Errorf("Validate() failed: %s", validateErr)
}
msg := []byte("test")
@ -373,19 +375,21 @@ func testEverything(t *testing.T, priv *PrivateKey) {
t.Errorf("DecryptPKCS1v15 accepted a long ciphertext")
}
der, err := x509.MarshalPKCS8PrivateKey(priv)
if err != nil {
t.Errorf("MarshalPKCS8PrivateKey: %v", err)
}
key, err := x509.ParsePKCS8PrivateKey(der)
if err != nil {
t.Errorf("ParsePKCS8PrivateKey: %v", err)
}
if !key.(*PrivateKey).Equal(priv) {
t.Errorf("private key mismatch")
if validateErr == nil {
der, err := x509.MarshalPKCS8PrivateKey(priv)
if err != nil {
t.Errorf("MarshalPKCS8PrivateKey: %v", err)
}
key, err := x509.ParsePKCS8PrivateKey(der)
if err != nil {
t.Errorf("ParsePKCS8PrivateKey: %v", err)
}
if !key.(*PrivateKey).Equal(priv) {
t.Errorf("private key mismatch")
}
}
der, err = x509.MarshalPKIXPublicKey(&priv.PublicKey)
der, err := x509.MarshalPKIXPublicKey(&priv.PublicKey)
if err != nil {
t.Errorf("MarshalPKIXPublicKey: %v", err)
}
@ -517,6 +521,30 @@ fLVGuFoTVIu2bF0cWAjNNMg=
var test2048Key = parseKey(test2048KeyPEM)
var test2048KeyOnlyD = &PrivateKey{
PublicKey: test2048Key.PublicKey,
D: test2048Key.D,
}
var test2048KeyWithoutPrecomputed = &PrivateKey{
PublicKey: test2048Key.PublicKey,
D: test2048Key.D,
Primes: test2048Key.Primes,
}
// test2048KeyWithPrecomputed is different from test2048Key because it includes
// only the public precomputed values, and not the fips140/rsa.PrivateKey.
var test2048KeyWithPrecomputed = &PrivateKey{
PublicKey: test2048Key.PublicKey,
D: test2048Key.D,
Primes: test2048Key.Primes,
Precomputed: PrecomputedValues{
Dp: test2048Key.Precomputed.Dp,
Dq: test2048Key.Precomputed.Dq,
Qinv: test2048Key.Precomputed.Qinv,
},
}
var test3072Key = parseKey(testingKey(`-----BEGIN TESTING KEY-----
MIIG/gIBADANBgkqhkiG9w0BAQEFAASCBugwggbkAgEAAoIBgQDJrvevql7G07LM
xQAwAA1Oo8qUAkWfmpgrpxIUZE1QTyMCDaspQJGBBR2+iStrzi2NnWvyBz3jJWFZ
@ -700,31 +728,15 @@ func BenchmarkEncryptOAEP(b *testing.B) {
func BenchmarkSignPKCS1v15(b *testing.B) {
b.Run("2048", func(b *testing.B) { benchmarkSignPKCS1v15(b, test2048Key) })
b.Run("2048/noprecomp/OnlyD", func(b *testing.B) {
benchmarkSignPKCS1v15(b, &PrivateKey{
PublicKey: test2048Key.PublicKey,
D: test2048Key.D,
})
benchmarkSignPKCS1v15(b, test2048KeyOnlyD)
})
b.Run("2048/noprecomp/Primes", func(b *testing.B) {
benchmarkSignPKCS1v15(b, &PrivateKey{
PublicKey: test2048Key.PublicKey,
D: test2048Key.D,
Primes: test2048Key.Primes,
})
benchmarkSignPKCS1v15(b, test2048KeyWithoutPrecomputed)
})
// This is different from "2048" because it's only the public precomputed
// values, and not the crypto/internal/fips140/rsa.PrivateKey.
b.Run("2048/noprecomp/AllValues", func(b *testing.B) {
benchmarkSignPKCS1v15(b, &PrivateKey{
PublicKey: test2048Key.PublicKey,
D: test2048Key.D,
Primes: test2048Key.Primes,
Precomputed: PrecomputedValues{
Dp: test2048Key.Precomputed.Dp,
Dq: test2048Key.Precomputed.Dq,
Qinv: test2048Key.Precomputed.Qinv,
},
})
benchmarkSignPKCS1v15(b, test2048KeyWithPrecomputed)
})
}
@ -1188,3 +1200,120 @@ cHPGX1uUDRAU1xxtpVQ0OqXyEgqwz6y6hYRw
testEverything(t, k1)
testEverything(t, k2)
}
func TestNotPrecomputed(t *testing.T) {
t.Run("OnlyD", func(t *testing.T) {
testEverything(t, test2048KeyOnlyD)
k := *test2048KeyOnlyD
k.Precompute()
testEverything(t, &k)
})
t.Run("Primes", func(t *testing.T) {
testEverything(t, test2048KeyWithoutPrecomputed)
k := *test2048KeyWithoutPrecomputed
k.Precompute()
if k.Precomputed.Dp == nil || k.Precomputed.Dq == nil || k.Precomputed.Qinv == nil {
t.Error("Precomputed values should not be nil after Precompute()")
}
testEverything(t, &k)
})
t.Run("AllValues", func(t *testing.T) {
testEverything(t, test2048KeyWithPrecomputed)
k := *test2048KeyWithoutPrecomputed
k.Precompute()
if k.Precomputed.Dp == nil || k.Precomputed.Dq == nil || k.Precomputed.Qinv == nil {
t.Error("Precomputed values should not be nil after Precompute()")
}
testEverything(t, &k)
})
}
func TestModifiedPrivateKey(t *testing.T) {
if test512Key.Validate() != nil {
t.Fatal("test512Key should be valid")
}
t.Run("PublicKey mismatch", func(t *testing.T) {
testModifiedPrivateKey(t, func(k *PrivateKey) {
k.PublicKey = test512KeyTwo.PublicKey
})
})
t.Run("Precomputed mismatch", func(t *testing.T) {
testModifiedPrivateKey(t, func(k *PrivateKey) {
k.Precomputed = test512KeyTwo.Precomputed
})
})
t.Run("N+2", func(t *testing.T) {
testModifiedPrivateKey(t, func(k *PrivateKey) {
k.N = new(big.Int).Add(k.N, big.NewInt(2))
})
})
t.Run("N=0", func(t *testing.T) {
testModifiedPrivateKey(t, func(k *PrivateKey) {
k.N = new(big.Int)
})
})
t.Run("N is nil", func(t *testing.T) {
testModifiedPrivateKey(t, func(k *PrivateKey) {
k.N = nil
})
})
t.Run("P+2", func(t *testing.T) {
testModifiedPrivateKey(t, func(k *PrivateKey) {
k.Primes[0] = new(big.Int).Add(k.Primes[0], big.NewInt(2))
})
})
t.Run("P=0", func(t *testing.T) {
testModifiedPrivateKey(t, func(k *PrivateKey) {
k.Primes[0] = new(big.Int)
})
})
t.Run("P is nil", func(t *testing.T) {
testModifiedPrivateKey(t, func(k *PrivateKey) {
k.Primes[0] = nil
})
})
t.Run("Q+2", func(t *testing.T) {
testModifiedPrivateKey(t, func(k *PrivateKey) {
k.Primes[1] = new(big.Int).Add(k.Primes[1], big.NewInt(2))
})
})
t.Run("Q=0", func(t *testing.T) {
testModifiedPrivateKey(t, func(k *PrivateKey) {
k.Primes[1] = new(big.Int)
})
})
t.Run("Q is nil", func(t *testing.T) {
testModifiedPrivateKey(t, func(k *PrivateKey) {
k.Primes[1] = nil
})
})
t.Run("E+2", func(t *testing.T) {
testModifiedPrivateKey(t, func(k *PrivateKey) {
k.E += 2
})
})
t.Run("E=0", func(t *testing.T) {
testModifiedPrivateKey(t, func(k *PrivateKey) {
k.E = 0
})
})
}
func testModifiedPrivateKey(t *testing.T, f func(*PrivateKey)) {
k := new(PrivateKey)
*k = *test512Key
k.Primes = slices.Clone(k.Primes)
f(k)
if err := k.Validate(); err == nil {
t.Error("Validate should have failed")
}
k.Precompute()
if err := k.Validate(); err == nil {
t.Error("Validate should have failed after Precompute()")
}
}