Skip to content

Commit

Permalink
Add cryptoutil/rsa, RSA key generation using a seeded DRBG (#141)
Browse files Browse the repository at this point in the history
* 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
sgmiller authored Nov 25, 2024
1 parent 98c833b commit e1e8729
Show file tree
Hide file tree
Showing 5 changed files with 187 additions and 0 deletions.
1 change: 1 addition & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ jobs:
module: ["awsutil",
"base62",
"configutil",
"cryptoutil",
"fileutil",
"gatedwriter",
"httputil",
Expand Down
14 changes: 14 additions & 0 deletions cryptoutil/go.mod
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
)
16 changes: 16 additions & 0 deletions cryptoutil/go.sum
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=
71 changes: 71 additions & 0 deletions cryptoutil/rsa.go
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)
}
85 changes: 85 additions & 0 deletions cryptoutil/rsa_test.go
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)
}
}

0 comments on commit e1e8729

Please sign in to comment.