-
Notifications
You must be signed in to change notification settings - Fork 24
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add cryptoutil/rsa, RSA key generation using a seeded DRBG (#141)
* Add an RSA key generation function which uses a DRBG to pick candidate primes rather than using a random source directly * Make unit test use the same bytes between methods for a direct comparison * wip * Add an RSA key generation function which uses a DRBG to pick candidate primes rather than using a random source directly * Make unit test use the same bytes between methods for a direct comparison * wip * formatting * typo and import fixes * typo * go 1.22 * Add a max reseed count * github action update * no need to name names * fix comment inaccuracy * one more comment
- Loading branch information
Showing
5 changed files
with
187 additions
and
0 deletions.
There are no files selected for viewing
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
module github.com/hashicorp/go-secure-stdlib/rsa | ||
|
||
go 1.22.0 | ||
|
||
require ( | ||
github.com/hashicorp/go-hmac-drbg v0.0.0-20210916214228-a6e5a68489f6 | ||
github.com/stretchr/testify v1.9.0 | ||
) | ||
|
||
require ( | ||
github.com/davecgh/go-spew v1.1.1 // indirect | ||
github.com/pmezard/go-difflib v1.0.0 // indirect | ||
gopkg.in/yaml.v3 v3.0.1 // indirect | ||
) |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | ||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||
github.com/hashicorp/go-hmac-drbg v0.0.0-20210916214228-a6e5a68489f6 h1:kBoJV4Xl5FLtBfnBjDvBxeNSy2IRITSGs73HQsFUEjY= | ||
github.com/hashicorp/go-hmac-drbg v0.0.0-20210916214228-a6e5a68489f6/go.mod h1:y+HSOcOGB48PkUxNyLAiCiY6rEENu+E+Ss4LG8QHwf4= | ||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= | ||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= | ||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= | ||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | ||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
// Copyright (c) HashiCorp, Inc. | ||
// SPDX-License-Identifier: MPL-2.0 | ||
|
||
package cryptoutil | ||
|
||
import ( | ||
"crypto/rand" | ||
"crypto/rsa" | ||
"fmt" | ||
"io" | ||
|
||
"github.com/hashicorp/go-hmac-drbg/hmacdrbg" | ||
) | ||
|
||
// Settable for testing | ||
var platformReader = rand.Reader | ||
|
||
const maxReseeds = 10000 // 7500 * 10000 * 8 = 600mm bits | ||
|
||
// GenerateRSAKeyWithHMACDRBG generates an RSA key with a deterministic random bit generator, seeded | ||
// with entropy from the provided random source. Some random bit sources are quite slow, for example | ||
// HSMs with true RNGs can take 500ms to produce enough bits to generate a single number | ||
// to test for primality, taking literally minutes to succeed in generating a key. As an example, when | ||
// testing this function, one run took 921 attempts to generate a 2048 bit RSA key, which would have taken | ||
// over 7 minutes on the HSM of the reporting customer. | ||
// | ||
// Instead, this function seeds a DRBG (specifically HMAC-DRBG from NIST SP800-90a) with | ||
// entropy from a random source, then uses the output of that DRBG to generate candidate primes. | ||
// This is still secure as the output of a DRBG is secure if the seed is sufficiently random, and | ||
// an attacker cannot predict which numbers are chosen for primes if they don't have access to the seed. | ||
// Additionally, the seed in this case is quite large indeed, 512 bits, well above what could be brute | ||
// forced. | ||
// | ||
// This is a sanctioned approach from FIPS 186-5 (A.1.2) | ||
func GenerateRSAKeyWithHMACDRBG(rand io.Reader, bits int) (*rsa.PrivateKey, error) { | ||
seed := make([]byte, (2*256)/8) // 2x maximum security strength (256-bits) from SP 800-57, Table 2 | ||
defer func() { | ||
// This may not work due to the GC but worth a shot | ||
for i := 0; i < len(seed); i++ { | ||
seed[i] = 0 | ||
} | ||
}() | ||
|
||
// Pretty unlikely to need even one reseed, but better to avoid an infinite loop. | ||
for i := 0; i < maxReseeds; i++ { | ||
if _, err := rand.Read(seed); err != nil { | ||
return nil, err | ||
} | ||
drbg := hmacdrbg.NewHmacDrbg(256, seed, []byte("generate-key-with-hmac-drbg")) | ||
reader := hmacdrbg.NewHmacDrbgReader(drbg) | ||
key, err := rsa.GenerateKey(reader, bits) | ||
if err != nil { | ||
if err.Error() == "MUST_RESEED" { | ||
// Oops, ran out of bytes (pretty unlikely but just in case) | ||
continue | ||
} | ||
return nil, err | ||
} | ||
return key, nil | ||
} | ||
return nil, fmt.Errorf("could not generate key after %d reseed of HMAC_DRBG", maxReseeds) | ||
} | ||
|
||
// GenerateRSAKey tests whether the random source is rand.Reader, and uses it directly if so (as it will | ||
// be a platform RNG and fast). If not, we assume it's some other slower source and use the HmacDRBG version. | ||
func GenerateRSAKey(randomSource io.Reader, bits int) (*rsa.PrivateKey, error) { | ||
if randomSource == platformReader { | ||
return rsa.GenerateKey(randomSource, bits) | ||
} | ||
return GenerateRSAKeyWithHMACDRBG(randomSource, bits) | ||
} |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
package cryptoutil | ||
|
||
import ( | ||
"bytes" | ||
"crypto/rand" | ||
"crypto/rsa" | ||
"testing" | ||
"time" | ||
|
||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
type slowRand struct { | ||
randomness *bytes.Buffer | ||
randomBytes []byte | ||
calls int | ||
} | ||
|
||
func newSlowRand() *slowRand { | ||
b := make([]byte, 10247680) | ||
rand.Read(b) | ||
sr := &slowRand{ | ||
randomBytes: b, | ||
} | ||
sr.Reset() | ||
return sr | ||
} | ||
|
||
func (s *slowRand) Reset() { | ||
s.calls = 0 | ||
s.randomness = bytes.NewBuffer(s.randomBytes) | ||
} | ||
|
||
var sr *slowRand | ||
|
||
func TestMain(m *testing.M) { | ||
sr = newSlowRand() | ||
m.Run() | ||
} | ||
|
||
func (s *slowRand) Read(p []byte) (n int, err error) { | ||
// First one is free | ||
if s.calls > 0 { | ||
time.Sleep(50 * time.Millisecond) | ||
} | ||
|
||
n, _ = s.randomness.Read(p) | ||
s.calls++ | ||
return | ||
} | ||
|
||
func TestGenerateKeyWithHMACDRBG(t *testing.T) { | ||
key, err := GenerateRSAKeyWithHMACDRBG(rand.Reader, 2048) | ||
require.NoError(t, err) | ||
require.Equal(t, 2048/8, key.Size()) | ||
key, err = GenerateRSAKey(rand.Reader, 2048) | ||
require.NoError(t, err) | ||
require.Equal(t, 2048/8, key.Size()) | ||
} | ||
|
||
func BenchmarkRSAKeyGeneration(b *testing.B) { | ||
sr.Reset() | ||
for i := 0; i < b.N; i++ { | ||
rsa.GenerateKey(sr, 2048) | ||
b.Logf("%d calls to the RNG, b.N=%d", sr.calls, b.N) | ||
} | ||
} | ||
|
||
func BenchmarkConditionalRSAKeyGeneration(b *testing.B) { | ||
platformReader = sr | ||
sr.Reset() | ||
for i := 0; i < b.N; i++ { | ||
GenerateRSAKey(sr, 2048) | ||
b.Logf("%d calls to the RNG, b.N=%d", sr.calls, b.N) | ||
} | ||
} | ||
|
||
func BenchmarkRSAKeyGenerationWithDRBG(b *testing.B) { | ||
sr.Reset() | ||
for i := 0; i < b.N; i++ { | ||
sr.calls = 0 | ||
GenerateRSAKeyWithHMACDRBG(sr, 2048) | ||
b.Logf("%d calls to the RNG, b.N=%d", sr.calls, b.N) | ||
} | ||
} |