From d80c8226b524401f3106f92ad00004bc37a56775 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sun, 2 Aug 2020 08:16:28 +0000 Subject: [PATCH] truly deterministic RSA keys --- mockid/api/common.go | 47 ++++++++++++++++++++++++-------------- mockid/api/generate.go | 23 +++++++++++++++---- mockid/mockid_test.go | 51 +++++++++++++++++++++++------------------- 3 files changed, 77 insertions(+), 44 deletions(-) diff --git a/mockid/api/common.go b/mockid/api/common.go index b2fca1e..651e619 100644 --- a/mockid/api/common.go +++ b/mockid/api/common.go @@ -9,19 +9,34 @@ import ( "io" "log" "math/rand" + mathrand "math/rand" "net/http" ) // options are the things that we may need to know about a request to fulfill it properly type options struct { - KeyType string `json:"kty"` - Seed int64 `json:"-"` - SeedStr string `json:"seed"` - rndReader io.Reader `json:"-"` + KeyType string `json:"kty"` + Seed int64 `json:"-"` + SeedStr string `json:"seed"` +} + +// this shananigans is only for testing and debug API stuff +func (o *options) nextRand() *mathrand.Rand { + if 0 == o.Seed { + panic(errors.New("called nextRand impossibly")) + return nil + } + return rand.New(rand.NewSource(o.Seed)) +} + +func (o *options) nextReader() io.Reader { + if 0 == o.Seed { + return RandomReader + } + return rand.New(rand.NewSource(o.Seed)) } func getOpts(r *http.Request) (*options, error) { - rndReader := RandomReader tok := make(map[string]interface{}) decoder := json.NewDecoder(r.Body) err := decoder.Decode(&tok) @@ -41,27 +56,25 @@ func getOpts(r *http.Request) (*options, error) { seed, _ = binary.ReadVarint(bytes.NewReader(b[0:8])) } + opts := &options{ + Seed: seed, + } + var n int if 0 != seed { - rnd := rand.New(rand.NewSource(seed)) - rndReader = rnd - n = rnd.Intn(2) + n = opts.nextRand().Intn(2) } else { n = rand.Intn(2) } - kty, _ := tok["kty"].(string) - if "" == kty { + opts.KeyType, _ = tok["kty"].(string) + if "" == opts.KeyType { if 0 == n { - kty = "RSA" + opts.KeyType = "RSA" } else { - kty = "EC" + opts.KeyType = "EC" } } - return &options{ - KeyType: kty, - Seed: seed, - rndReader: rndReader, - }, nil + return opts, nil } diff --git a/mockid/api/generate.go b/mockid/api/generate.go index edd97b9..4358ecb 100644 --- a/mockid/api/generate.go +++ b/mockid/api/generate.go @@ -38,7 +38,7 @@ func GeneratePrivateJWK(w http.ResponseWriter, r *http.Request) { // GeneratePrivateDER will create a new private key in a valid DER encoding func GeneratePrivateDER(w http.ResponseWriter, r *http.Request) { - log.Printf("%s %s\n", r.Method, r.URL.Path) + log.Printf("%s %s", r.Method, r.URL.Path) if "POST" != r.Method { http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) return @@ -58,7 +58,7 @@ func GeneratePrivateDER(w http.ResponseWriter, r *http.Request) { // GeneratePrivatePEM will create a new private key in a valid PEM encoding func GeneratePrivatePEM(w http.ResponseWriter, r *http.Request) { - log.Printf("%s %s\n", r.Method, r.URL.Path) + log.Printf("%s %s", r.Method, r.URL.Path) if "POST" != r.Method { http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) return @@ -80,9 +80,24 @@ func genPrivKey(opts *options) keypairs.PrivateKey { var privkey keypairs.PrivateKey if "RSA" == opts.KeyType { keylen := 2048 - privkey, _ = rsa.GenerateKey(opts.rndReader, keylen) + rndReader := opts.nextReader() + privkey, _ = rsa.GenerateKey(rndReader, keylen) + if rndReader != RandomReader { + for i := 0; i < 16; i++ { + otherkey, _ := rsa.GenerateKey(opts.nextRand(), keylen) + otherCmp := otherkey.D.Cmp(privkey.(*rsa.PrivateKey).D) + if 0 != otherCmp { + // There are two possible keys, choose the lesser D value + // See https://github.com/square/go-jose/issues/189 + if otherCmp < 0 { + privkey = otherkey + } + break + } + } + } } else { - privkey, _ = ecdsa.GenerateKey(elliptic.P256(), opts.rndReader) + privkey, _ = ecdsa.GenerateKey(elliptic.P256(), opts.nextReader()) } return privkey } diff --git a/mockid/mockid_test.go b/mockid/mockid_test.go index db6dc69..b26297b 100644 --- a/mockid/mockid_test.go +++ b/mockid/mockid_test.go @@ -121,7 +121,7 @@ func TestGenWithSeed(t *testing.T) { res, err := client.Do(&http.Request{ Method: "POST", URL: urlstr, - Body: ioutil.NopCloser(bytes.NewReader([]byte(`{"seed":"c"}`))), + Body: ioutil.NopCloser(bytes.NewReader([]byte(`{"seed":"test"}`))), }) if nil != err { //t.Fatal(err) @@ -135,29 +135,34 @@ func TestGenWithSeed(t *testing.T) { return } - // Key B - client = srv.Client() - urlstr, _ = url.Parse(srv.URL + "/private.jwk.json") - res, err = client.Do(&http.Request{ - Method: "POST", - URL: urlstr, - Body: ioutil.NopCloser(bytes.NewReader([]byte(`{"seed":"c"}`))), - }) - if nil != err { - //t.Fatal(err) - t.Error(err) - return - } - dataB, err := ioutil.ReadAll(res.Body) - if nil != err { - //t.Fatal(err) - t.Error(err) - return - } + // See https://github.com/square/go-jose/issues/189 + for i := 0; i < 8; i++ { + // Key B + client = srv.Client() + urlstr, _ = url.Parse(srv.URL + "/private.jwk.json") + res, err = client.Do(&http.Request{ + Method: "POST", + URL: urlstr, + Body: ioutil.NopCloser(bytes.NewReader([]byte(`{"seed":"test"}`))), + }) + if nil != err { + //t.Fatal(err) + t.Error(err) + return + } + dataB, err := ioutil.ReadAll(res.Body) + if nil != err { + //t.Fatal(err) + t.Error(err) + return + } - if '{' != dataA[0] || len(dataA) < 100 || string(dataA) != string(dataB) { - t.Error(errors.New("keys with identical seeds should be identical")) - return + if '{' != dataA[0] || len(dataA) < 100 || string(dataA) != string(dataB) { + log.Println(string(dataA)) + log.Println(string(dataB)) + t.Error(errors.New("keys with identical seeds should be identical")) + return + } } }