From 9b250c8cbba9b14f91c26df8a76370d632939809 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sun, 2 Aug 2020 09:39:56 +0000 Subject: [PATCH] add api and tests for generating public keys --- mockid/api/common.go | 13 ++---- mockid/api/generate.go | 97 ++++++++++++++++++++++++++++++++++---- mockid/mockid_test.go | 103 ++++++++++++++++++++++++++++++++++++++--- mockid/route.go | 6 ++- xkeypairs/marshal.go | 45 ++++++++++++++++++ 5 files changed, 239 insertions(+), 25 deletions(-) diff --git a/mockid/api/common.go b/mockid/api/common.go index 651e619..b5ef199 100644 --- a/mockid/api/common.go +++ b/mockid/api/common.go @@ -15,20 +15,13 @@ import ( // options are the things that we may need to know about a request to fulfill it properly type options struct { + Key string `json:"key"` 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 @@ -56,13 +49,15 @@ func getOpts(r *http.Request) (*options, error) { seed, _ = binary.ReadVarint(bytes.NewReader(b[0:8])) } + key, _ := tok["key"].(string) opts := &options{ Seed: seed, + Key: key, } var n int if 0 != seed { - n = opts.nextRand().Intn(2) + n = opts.nextReader().(*mathrand.Rand).Intn(2) } else { n = rand.Intn(2) } diff --git a/mockid/api/generate.go b/mockid/api/generate.go index 4358ecb..55b7398 100644 --- a/mockid/api/generate.go +++ b/mockid/api/generate.go @@ -16,9 +16,31 @@ import ( // RandomReader may be overwritten for testing var RandomReader io.Reader = rand.Reader +// GeneratePublicJWK will create a new private key in JWK format +func GeneratePublicJWK(w http.ResponseWriter, r *http.Request) { + 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, err := getPrivKey(opts) + if nil != err { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + jwk := keypairs.MarshalJWKPublicKey(keypairs.NewPublicKey(privkey.Public())) + w.Write(append(jwk, '\n')) +} + // 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 @@ -36,9 +58,32 @@ func GeneratePrivateJWK(w http.ResponseWriter, r *http.Request) { w.Write(append(jwk, '\n')) } +// GeneratePublicDER will create a new private key in JWK format +func GeneratePublicDER(w http.ResponseWriter, r *http.Request) { + 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, err := getPrivKey(opts) + if nil != err { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + b, _ := xkeypairs.MarshalDERPublicKey(privkey.Public()) + + w.Write(b) +} + // GeneratePrivateDER will create a new private key in a valid DER encoding func GeneratePrivateDER(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 @@ -56,9 +101,32 @@ func GeneratePrivateDER(w http.ResponseWriter, r *http.Request) { w.Write(der) } +// GeneratePublicPEM will create a new private key in JWK format +func GeneratePublicPEM(w http.ResponseWriter, r *http.Request) { + 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, err := getPrivKey(opts) + if nil != err { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + b, _ := xkeypairs.MarshalPEMPublicKey(privkey.Public()) + + w.Write(b) +} + // GeneratePrivatePEM will create a new private key in a valid PEM encoding func GeneratePrivatePEM(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 @@ -76,15 +144,24 @@ func GeneratePrivatePEM(w http.ResponseWriter, r *http.Request) { w.Write(privpem) } +const maxRetry = 16 + +func getPrivKey(opts *options) (keypairs.PrivateKey, error) { + if "" != opts.Key { + return keypairs.ParsePrivateKey([]byte(opts.Key)) + } + return genPrivKey(opts), nil +} + func genPrivKey(opts *options) keypairs.PrivateKey { var privkey keypairs.PrivateKey + if "RSA" == opts.KeyType { keylen := 2048 - rndReader := opts.nextReader() - privkey, _ = rsa.GenerateKey(rndReader, keylen) - if rndReader != RandomReader { - for i := 0; i < 16; i++ { - otherkey, _ := rsa.GenerateKey(opts.nextRand(), keylen) + privkey, _ = rsa.GenerateKey(opts.nextReader(), keylen) + if 0 != opts.Seed { + for i := 0; i < maxRetry; i++ { + otherkey, _ := rsa.GenerateKey(opts.nextReader(), keylen) otherCmp := otherkey.D.Cmp(privkey.(*rsa.PrivateKey).D) if 0 != otherCmp { // There are two possible keys, choose the lesser D value @@ -94,9 +171,13 @@ func genPrivKey(opts *options) keypairs.PrivateKey { } break } + if maxRetry == i-1 { + log.Printf("error: coinflip landed on heads %d times", maxRetry) + } } } } else { + // TODO: EC keys may also suffer the same random problems in the future privkey, _ = ecdsa.GenerateKey(elliptic.P256(), opts.nextReader()) } return privkey diff --git a/mockid/mockid_test.go b/mockid/mockid_test.go index b26297b..e40171d 100644 --- a/mockid/mockid_test.go +++ b/mockid/mockid_test.go @@ -103,10 +103,10 @@ func TestGenerateJWK(t *testing.T) { switch key.(type) { case *rsa.PrivateKey: // no-op - log.Println("is RSA") + //log.Println("is RSA") case *ecdsa.PrivateKey: // no-op - log.Println("is EC") + //log.Println("is EC") default: t.Fatal(errors.New("impossible key type")) } @@ -243,13 +243,104 @@ func TestGeneratePEM(t *testing.T) { switch key.(type) { case *rsa.PrivateKey: // no-op - log.Println("is RSA") + //log.Println("is RSA") case *ecdsa.PrivateKey: // no-op - log.Println("is EC") + //log.Println("is EC") + default: + t.Fatal(errors.New("impossible key type")) + } +} + +func TestPublicJWKWithKey(t *testing.T) { + client := srv.Client() + urlstr, _ := url.Parse(srv.URL + "/public.jwk.json") + //fmt.Println("URL:", srv.URL, urlstr) + res, err := client.Do(&http.Request{ + Method: "POST", + URL: urlstr, + Body: ioutil.NopCloser(bytes.NewReader([]byte(`{"key":"{\"crv\":\"P-256\",\"d\":\"s0YhjGUJpp6OvyuNS_4igrc7ddDZy5N2ANxoQm7E5sc\",\"kty\":\"EC\",\"x\":\"hPsE4OMhpd2TvrhjDgr1BhF-L1n4O-gPm1flwTh5kzo\",\"y\":\"BWZ1naEJuNOdnQ4HmbHavqdLKxoj77Fu8mkJPjSuh54\"}"}`))), + }) + if nil != err { + //t.Fatal(err) + t.Error(err) + return + } + + data, err := ioutil.ReadAll(res.Body) + if nil != err { + //t.Fatal(err) + t.Error(err) + return + } + + jwk := map[string]string{} + err = json.Unmarshal(data, &jwk) + if nil != err { + //t.Fatal(err) + t.Error(err) + return + } + + if "" != jwk["d"] { + t.Fatal("Has private key 'd' from supposed public key") + } + + if "hPsE4OMhpd2TvrhjDgr1BhF-L1n4O-gPm1flwTh5kzo" != jwk["x"] { + t.Fatal("Missing public key 'x' or 'e' from supposed public key") + } + + key, err := keypairs.ParsePublicKey(data) + if nil != err { + t.Error(err) + return + } + + switch key.Key().(type) { + case *ecdsa.PublicKey: + // no-op + //log.Println("is EC") + default: + t.Fatal(errors.New("impossible key type")) + } +} + +func TestPublicPEMWithSeed(t *testing.T) { + client := srv.Client() + urlstr, _ := url.Parse(srv.URL + "/pub.pem") + //fmt.Println("URL:", srv.URL, urlstr) + 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 + } + + data, err := ioutil.ReadAll(res.Body) + if nil != err { + //t.Fatal(err) + t.Error(err) + return + } + + key, err := keypairs.ParsePublicKey(data) + if nil != err { + t.Error(err) + return + } + + switch key.Key().(type) { + case *rsa.PublicKey: + // no-op + //log.Println("is RSA") + case *ecdsa.PublicKey: + // no-op + //log.Println("is EC") default: t.Fatal(errors.New("impossible key type")) } - - //fmt.Printf("%#v\n", key) } diff --git a/mockid/route.go b/mockid/route.go index 17b7d79..f3fc36c 100644 --- a/mockid/route.go +++ b/mockid/route.go @@ -182,11 +182,13 @@ func Route(jwksPrefix string, privkey keypairs.PrivateKey) http.Handler { }) http.HandleFunc("/private.jwk.json", api.GeneratePrivateJWK) - http.HandleFunc("/priv.der", api.GeneratePrivateDER) - http.HandleFunc("/priv.pem", api.GeneratePrivatePEM) + http.HandleFunc("/public.jwk.json", api.GeneratePublicJWK) + http.HandleFunc("/pub.der", api.GeneratePublicDER) + http.HandleFunc("/pub.pem", api.GeneratePublicPEM) + http.HandleFunc("/inspect_token", func(w http.ResponseWriter, r *http.Request) { token := r.Header.Get("Authorization") log.Printf("%s %s %s\n", r.Method, r.URL.Path, token) diff --git a/xkeypairs/marshal.go b/xkeypairs/marshal.go index 3da61b7..3902785 100644 --- a/xkeypairs/marshal.go +++ b/xkeypairs/marshal.go @@ -1,6 +1,7 @@ package xkeypairs import ( + "crypto" "crypto/ecdsa" "crypto/rsa" "crypto/x509" @@ -14,6 +15,50 @@ import ( "git.rootprojects.org/root/keypairs" ) +// MarshalPEMPublicKey outputs the given public key as JWK +func MarshalPEMPublicKey(pubkey crypto.PublicKey) ([]byte, error) { + block, err := marshalDERPublicKey(pubkey) + if nil != err { + return nil, err + } + return pem.EncodeToMemory(block), nil +} + +// MarshalDERPublicKey outputs the given public key as JWK +func MarshalDERPublicKey(pubkey crypto.PublicKey) ([]byte, error) { + block, err := marshalDERPublicKey(pubkey) + if nil != err { + return nil, err + } + return block.Bytes, nil +} + +// marshalDERPublicKey outputs the given public key as JWK +func marshalDERPublicKey(pubkey crypto.PublicKey) (*pem.Block, error) { + + var der []byte + var typ string + var err error + switch k := pubkey.(type) { + case *rsa.PublicKey: + der = x509.MarshalPKCS1PublicKey(k) + typ = "RSA PUBLIC KEY" + case *ecdsa.PublicKey: + typ = "PUBLIC KEY" + der, err = x509.MarshalPKIXPublicKey(k) + if nil != err { + return nil, err + } + default: + panic("Developer Error: impossible key type") + } + + return &pem.Block{ + Bytes: der, + Type: typ, + }, nil +} + // MarshalJWKPrivateKey outputs the given private key as JWK func MarshalJWKPrivateKey(privkey keypairs.PrivateKey) []byte { // thumbprint keys are alphabetically sorted and only include the necessary public parts