From da712abbb216bab748c9a72bee70b2900d4ea0a6 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sat, 1 Aug 2020 23:59:20 +0000 Subject: [PATCH] add seed for random key generator (tested) --- mockid.go | 3 +- mockid/api/common.go | 62 +++++++++++++++++ mockid/api/generate.go | 88 +++++++++++++++++++++++ mockid/mockid.go | 3 +- mockid/mockid_test.go | 100 +++++++++++++++++++++++++- mockid/route.go | 116 ++++--------------------------- {mockid => xkeypairs}/marshal.go | 2 +- {mockid => xkeypairs}/parse.go | 5 +- 8 files changed, 269 insertions(+), 110 deletions(-) create mode 100644 mockid/api/common.go create mode 100644 mockid/api/generate.go rename {mockid => xkeypairs}/marshal.go (99%) rename {mockid => xkeypairs}/parse.go (76%) diff --git a/mockid.go b/mockid.go index 3bd8488..16e6490 100644 --- a/mockid.go +++ b/mockid.go @@ -13,6 +13,7 @@ import ( "time" "git.coolaj86.com/coolaj86/go-mockid/mockid" + "git.coolaj86.com/coolaj86/go-mockid/xkeypairs" "git.rootprojects.org/root/keypairs" _ "github.com/joho/godotenv/autoload" @@ -91,7 +92,7 @@ func main() { }() // TODO privB := keypairs.MarshalJWKPrivateKey(privkey) - privB := mockid.MarshalJWKPrivateKey(privkey) + privB := xkeypairs.MarshalJWKPrivateKey(privkey) fmt.Printf("Private Key:\n\t%s\n", string(privB)) pubB := keypairs.MarshalJWKPublicKey(keypairs.NewPublicKey(privkey.Public())) fmt.Printf("Public Key:\n\t%s\n", string(pubB)) diff --git a/mockid/api/common.go b/mockid/api/common.go new file mode 100644 index 0000000..8d6b82e --- /dev/null +++ b/mockid/api/common.go @@ -0,0 +1,62 @@ +package api + +import ( + "bytes" + "crypto/sha256" + "encoding/binary" + "encoding/json" + "errors" + "io" + "log" + "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:"-"` +} + +func getOpts(r *http.Request) (*options, error) { + rndReader := RandomReader + tok := make(map[string]interface{}) + decoder := json.NewDecoder(r.Body) + err := decoder.Decode(&tok) + if nil != err && io.EOF != err { + log.Printf("json decode error: %s", err) + return nil, errors.New("Bad Request: invalid json body") + } + defer r.Body.Close() + + var seed int64 + seedStr, _ := tok["seed"].(string) + if "" != seedStr { + if len(seedStr) > 256 { + return nil, errors.New("Bad Request: base64 seed should be <256 characters (and is truncated to 64-bits anyway)") + } + b := sha256.Sum256([]byte(seedStr)) + seed, _ = binary.ReadVarint(bytes.NewReader(b[0:8])) + } + + if 0 != seed { + rndReader = rand.New(rand.NewSource(seed)) + } + + kty, _ := tok["kty"].(string) + if "" == kty { + if 0 == rand.Intn(2) { + kty = "RSA" + } else { + kty = "EC" + } + } + + return &options{ + KeyType: kty, + Seed: seed, + rndReader: rndReader, + }, nil +} diff --git a/mockid/api/generate.go b/mockid/api/generate.go new file mode 100644 index 0000000..edd97b9 --- /dev/null +++ b/mockid/api/generate.go @@ -0,0 +1,88 @@ +package api + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "io" + "log" + "net/http" + + "git.coolaj86.com/coolaj86/go-mockid/xkeypairs" + "git.rootprojects.org/root/keypairs" +) + +// RandomReader may be overwritten for testing +var RandomReader io.Reader = rand.Reader + +// GeneratePrivateJWK will create a new private key in JWK format +func GeneratePrivateJWK(w http.ResponseWriter, r *http.Request) { + log.Printf("%s %s", r.Method, r.URL.Path) + if "POST" != r.Method { + http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) + return + } + + opts, err := getOpts(r) + if nil != err { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + privkey := genPrivKey(opts) + + jwk := xkeypairs.MarshalJWKPrivateKey(privkey) + w.Write(append(jwk, '\n')) +} + +// 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) + if "POST" != r.Method { + http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) + return + } + + opts, err := getOpts(r) + if nil != err { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + privkey := genPrivKey(opts) + + der, _ := xkeypairs.MarshalDERPrivateKey(privkey) + w.Write(der) +} + +// 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) + if "POST" != r.Method { + http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) + return + } + + opts, err := getOpts(r) + if nil != err { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + privkey := genPrivKey(opts) + + privpem, _ := xkeypairs.MarshalPEMPrivateKey(privkey) + w.Write(privpem) +} + +func genPrivKey(opts *options) keypairs.PrivateKey { + var privkey keypairs.PrivateKey + if "RSA" == opts.KeyType { + keylen := 2048 + privkey, _ = rsa.GenerateKey(opts.rndReader, keylen) + } else { + privkey, _ = ecdsa.GenerateKey(elliptic.P256(), opts.rndReader) + } + return privkey +} diff --git a/mockid/mockid.go b/mockid/mockid.go index cf1c7ea..4ff4b96 100644 --- a/mockid/mockid.go +++ b/mockid/mockid.go @@ -18,6 +18,7 @@ import ( "sync" "time" + "git.coolaj86.com/coolaj86/go-mockid/xkeypairs" "git.rootprojects.org/root/keypairs" //jwt "github.com/dgrijalva/jwt-go" ) @@ -94,7 +95,7 @@ func GenToken(host string, privkey keypairs.PrivateKey, query url.Values) (strin protected := fmt.Sprintf(`{"typ":"JWT","alg":%q,"kid":"%s"}`, alg, thumbprint) protected64 := base64.RawURLEncoding.EncodeToString([]byte(protected)) - exp, err := parseExp(query.Get("exp")) + exp, err := xkeypairs.ParseDuration(query.Get("exp")) if nil != err { // cryptic error code // TODO propagate error diff --git a/mockid/mockid_test.go b/mockid/mockid_test.go index 865244d..9e45588 100644 --- a/mockid/mockid_test.go +++ b/mockid/mockid_test.go @@ -1,6 +1,7 @@ package mockid import ( + "bytes" "crypto/ecdsa" "crypto/elliptic" "crypto/rsa" @@ -16,6 +17,8 @@ import ( "os" "testing" + "git.coolaj86.com/coolaj86/go-mockid/mockid/api" + "git.coolaj86.com/coolaj86/go-mockid/xkeypairs" "git.rootprojects.org/root/keypairs" //keypairs "github.com/big-squid/go-keypairs" //"github.com/big-squid/go-keypairs/keyfetch/uncached" @@ -32,6 +35,7 @@ func (TestReader) Read(p []byte) (n int, err error) { var testrnd = TestReader{} func init() { + api.RandomReader = testrnd rndsrc = testrnd } @@ -110,6 +114,100 @@ func TestGenerateJWK(t *testing.T) { //fmt.Printf("%#v\n", jwk) } +func TestGenWithSeed(t *testing.T) { + // Key A + 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 + } + dataA, err := ioutil.ReadAll(res.Body) + if nil != err { + //t.Fatal(err) + t.Error(err) + 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":"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 + } +} + +func TestGenWithRand(t *testing.T) { + // Key A + 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":""}`))), + }) + if nil != err { + //t.Fatal(err) + t.Error(err) + return + } + dataA, err := ioutil.ReadAll(res.Body) + if nil != err { + //t.Fatal(err) + t.Error(err) + 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":""}`))), + }) + 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 string(dataA) == string(dataB) { + t.Error(errors.New("keys with identical seeds should yield identical keys")) + return + } +} + func TestGeneratePEM(t *testing.T) { client := srv.Client() urlstr, _ := url.Parse(srv.URL + "/priv.pem") @@ -131,7 +229,7 @@ func TestGeneratePEM(t *testing.T) { return } - key, err := ParsePEMPrivateKey(data) + key, err := xkeypairs.ParsePEMPrivateKey(data) if nil != err { t.Error(err) return diff --git a/mockid/route.go b/mockid/route.go index 1429810..17b7d79 100644 --- a/mockid/route.go +++ b/mockid/route.go @@ -1,20 +1,14 @@ package mockid import ( - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rsa" "crypto/sha1" "crypto/sha256" "crypto/sha512" "encoding/base64" "encoding/json" - "errors" "fmt" - "io" "io/ioutil" "log" - mathrand "math/rand" "net/http" "os" "path/filepath" @@ -22,11 +16,14 @@ import ( "strings" "time" + "git.coolaj86.com/coolaj86/go-mockid/mockid/api" + "git.coolaj86.com/coolaj86/go-mockid/xkeypairs" "git.rootprojects.org/root/keypairs" "git.rootprojects.org/root/keypairs/keyfetch" "github.com/google/uuid" ) +// Route returns an HTTP Mux containing the full API func Route(jwksPrefix string, privkey keypairs.PrivateKey) http.Handler { Init() @@ -44,12 +41,12 @@ func Route(jwksPrefix string, privkey keypairs.PrivateKey) http.Handler { // is this the expiration of the nonce itself? methinks maybe so //res.setHeader("Expires", "Sun, 10 Mar 2019 08:04:45 GMT"); // TODO use one of the registered domains - //var indexUrl = "https://acme-staging-v02.api.letsencrypt.org/index" + //var indexURL = "https://acme-staging-v02.api.letsencrypt.org/index" */ //var port = (state.config.ipc && state.config.ipc.port || state._ipc.port || undefined); - //var indexUrl = "http://localhost:" + port + "/index"; - indexUrl := baseURL + "/index" - w.Header().Set("Link", "<"+indexUrl+">;rel=\"index\"") + //var indexURL = "http://localhost:" + port + "/index"; + indexURL := baseURL + "/index" + w.Header().Set("Link", "<"+indexURL+">;rel=\"index\"") w.Header().Set("Cache-Control", "max-age=0, no-cache, no-store") w.Header().Set("Pragma", "no-cache") //res.setHeader("Strict-Transport-Security", "max-age=604800"); @@ -184,101 +181,11 @@ func Route(jwksPrefix string, privkey keypairs.PrivateKey) http.Handler { fmt.Fprintf(w, token) }) - getKty := func(r *http.Request) (string, error) { - tok := make(map[string]interface{}) - decoder := json.NewDecoder(r.Body) - err := decoder.Decode(&tok) - if nil != err && io.EOF != err { - log.Printf("json decode error: %s", err) - return "", errors.New("Bad Request: invalid json body") - } - defer r.Body.Close() + http.HandleFunc("/private.jwk.json", api.GeneratePrivateJWK) - kty, _ := tok["kty"].(string) - if "" == kty { - if 0 == mathrand.Intn(2) { - kty = "RSA" - } else { - kty = "EC" - } - } - return kty, nil - } + http.HandleFunc("/priv.der", api.GeneratePrivateDER) - http.HandleFunc("/private.jwk.json", func(w http.ResponseWriter, r *http.Request) { - log.Printf("%s %s", r.Method, r.URL.Path) - if "POST" != r.Method { - http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) - return - } - - kty, err := getKty(r) - if nil != err { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - var privkey keypairs.PrivateKey - if "RSA" == kty { - keylen := 2048 - privkey, _ = rsa.GenerateKey(rndsrc, keylen) - } else { - privkey, _ = ecdsa.GenerateKey(elliptic.P256(), rndsrc) - } - - jwk := MarshalJWKPrivateKey(privkey) - w.Write(append(jwk, '\n')) - }) - - http.HandleFunc("/priv.der", func(w http.ResponseWriter, r *http.Request) { - log.Printf("%s %s\n", r.Method, r.URL.Path) - if "POST" != r.Method { - http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) - return - } - - kty, err := getKty(r) - if nil != err { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - var privkey keypairs.PrivateKey - if "RSA" == kty { - keylen := 2048 - privkey, _ = rsa.GenerateKey(rndsrc, keylen) - } else { - privkey, _ = ecdsa.GenerateKey(elliptic.P256(), rndsrc) - } - - der, _ := MarshalDERPrivateKey(privkey) - w.Write(der) - }) - - http.HandleFunc("/priv.pem", func(w http.ResponseWriter, r *http.Request) { - log.Printf("%s %s\n", r.Method, r.URL.Path) - if "POST" != r.Method { - http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) - return - } - - kty, err := getKty(r) - if nil != err { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - var privkey keypairs.PrivateKey - if "RSA" == kty { - keylen := 2048 - privkey, _ = rsa.GenerateKey(rndsrc, keylen) - } else { - privkey, _ = ecdsa.GenerateKey(elliptic.P256(), rndsrc) - } - - privpem, _ := MarshalPEMPrivateKey(privkey) - w.Write(privpem) - }) + http.HandleFunc("/priv.pem", api.GeneratePrivatePEM) http.HandleFunc("/inspect_token", func(w http.ResponseWriter, r *http.Request) { token := r.Header.Get("Authorization") @@ -404,7 +311,7 @@ func Route(jwksPrefix string, privkey keypairs.PrivateKey) http.Handler { http.HandleFunc("/key.jwk.json", func(w http.ResponseWriter, r *http.Request) { log.Printf("%s %s", r.Method, r.URL.Path) - jwk := string(MarshalJWKPrivateKey(privkey)) + jwk := string(xkeypairs.MarshalJWKPrivateKey(privkey)) jwk = strings.Replace(jwk, `{"`, `{ "`, 1) jwk = strings.Replace(jwk, `",`, `", `, -1) jwk = jwk[0 : len(jwk)-1] @@ -474,6 +381,7 @@ func getBaseURL(r *http.Request) string { ) } +// HTTPError describes an error that should be propagated to the HTTP client type HTTPError struct { message string code int diff --git a/mockid/marshal.go b/xkeypairs/marshal.go similarity index 99% rename from mockid/marshal.go rename to xkeypairs/marshal.go index 3db5cb5..3da61b7 100644 --- a/mockid/marshal.go +++ b/xkeypairs/marshal.go @@ -1,4 +1,4 @@ -package mockid +package xkeypairs import ( "crypto/ecdsa" diff --git a/mockid/parse.go b/xkeypairs/parse.go similarity index 76% rename from mockid/parse.go rename to xkeypairs/parse.go index ad565d0..d561404 100644 --- a/mockid/parse.go +++ b/xkeypairs/parse.go @@ -1,4 +1,4 @@ -package mockid +package xkeypairs import ( "strconv" @@ -6,12 +6,13 @@ import ( "git.rootprojects.org/root/keypairs" ) +// ParsePEMPrivateKey will parse a PEM Private Key (or JWK or DER) but in future versions will fail to parse other key input types func ParsePEMPrivateKey(block []byte) (keypairs.PrivateKey, error) { // TODO do not parse DER or JWK return keypairs.ParsePrivateKey(block) } -func parseExp(exp string) (int, error) { +func ParseDuration(exp string) (int, error) { if "" == exp { exp = "15m" }