Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add cryptoutil/rsa, RSA key generation using a seeded DRBG #141

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 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
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=
69 changes: 69 additions & 0 deletions cryptoutil/rsa.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// 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 a Thales HSM.
//
// 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, 1000 bits, well above what could be brute
// forced.
//
// This is a sanctioned approach from FIPS 186-4 (B.3.2)
func GenerateRSAKeyWithHMACDRBG(rand io.Reader, bits int) (*rsa.PrivateKey, error) {
seed := make([]byte, (2*256)/8) // 2x maximum security strength 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
}
}()
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)
}
}
Loading