add api and tests for generating public keys

This commit is contained in:
AJ ONeal 2020-08-02 09:39:56 +00:00
parent d80c8226b5
commit 9b250c8cbb
5 changed files with 239 additions and 25 deletions

View File

@ -15,20 +15,13 @@ import (
// options are the things that we may need to know about a request to fulfill it properly // options are the things that we may need to know about a request to fulfill it properly
type options struct { type options struct {
Key string `json:"key"`
KeyType string `json:"kty"` KeyType string `json:"kty"`
Seed int64 `json:"-"` Seed int64 `json:"-"`
SeedStr string `json:"seed"` SeedStr string `json:"seed"`
} }
// this shananigans is only for testing and debug API stuff // 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 { func (o *options) nextReader() io.Reader {
if 0 == o.Seed { if 0 == o.Seed {
return RandomReader return RandomReader
@ -56,13 +49,15 @@ func getOpts(r *http.Request) (*options, error) {
seed, _ = binary.ReadVarint(bytes.NewReader(b[0:8])) seed, _ = binary.ReadVarint(bytes.NewReader(b[0:8]))
} }
key, _ := tok["key"].(string)
opts := &options{ opts := &options{
Seed: seed, Seed: seed,
Key: key,
} }
var n int var n int
if 0 != seed { if 0 != seed {
n = opts.nextRand().Intn(2) n = opts.nextReader().(*mathrand.Rand).Intn(2)
} else { } else {
n = rand.Intn(2) n = rand.Intn(2)
} }

View File

@ -16,9 +16,31 @@ import (
// RandomReader may be overwritten for testing // RandomReader may be overwritten for testing
var RandomReader io.Reader = rand.Reader 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 // GeneratePrivateJWK will create a new private key in JWK format
func GeneratePrivateJWK(w http.ResponseWriter, r *http.Request) { func GeneratePrivateJWK(w http.ResponseWriter, r *http.Request) {
log.Printf("%s %s", r.Method, r.URL.Path)
if "POST" != r.Method { if "POST" != r.Method {
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
return return
@ -36,9 +58,32 @@ func GeneratePrivateJWK(w http.ResponseWriter, r *http.Request) {
w.Write(append(jwk, '\n')) 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 // GeneratePrivateDER will create a new private key in a valid DER encoding
func GeneratePrivateDER(w http.ResponseWriter, r *http.Request) { func GeneratePrivateDER(w http.ResponseWriter, r *http.Request) {
log.Printf("%s %s", r.Method, r.URL.Path)
if "POST" != r.Method { if "POST" != r.Method {
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
return return
@ -56,9 +101,32 @@ func GeneratePrivateDER(w http.ResponseWriter, r *http.Request) {
w.Write(der) 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 // GeneratePrivatePEM will create a new private key in a valid PEM encoding
func GeneratePrivatePEM(w http.ResponseWriter, r *http.Request) { func GeneratePrivatePEM(w http.ResponseWriter, r *http.Request) {
log.Printf("%s %s", r.Method, r.URL.Path)
if "POST" != r.Method { if "POST" != r.Method {
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
return return
@ -76,15 +144,24 @@ func GeneratePrivatePEM(w http.ResponseWriter, r *http.Request) {
w.Write(privpem) 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 { func genPrivKey(opts *options) keypairs.PrivateKey {
var privkey keypairs.PrivateKey var privkey keypairs.PrivateKey
if "RSA" == opts.KeyType { if "RSA" == opts.KeyType {
keylen := 2048 keylen := 2048
rndReader := opts.nextReader() privkey, _ = rsa.GenerateKey(opts.nextReader(), keylen)
privkey, _ = rsa.GenerateKey(rndReader, keylen) if 0 != opts.Seed {
if rndReader != RandomReader { for i := 0; i < maxRetry; i++ {
for i := 0; i < 16; i++ { otherkey, _ := rsa.GenerateKey(opts.nextReader(), keylen)
otherkey, _ := rsa.GenerateKey(opts.nextRand(), keylen)
otherCmp := otherkey.D.Cmp(privkey.(*rsa.PrivateKey).D) otherCmp := otherkey.D.Cmp(privkey.(*rsa.PrivateKey).D)
if 0 != otherCmp { if 0 != otherCmp {
// There are two possible keys, choose the lesser D value // There are two possible keys, choose the lesser D value
@ -94,9 +171,13 @@ func genPrivKey(opts *options) keypairs.PrivateKey {
} }
break break
} }
if maxRetry == i-1 {
log.Printf("error: coinflip landed on heads %d times", maxRetry)
}
} }
} }
} else { } else {
// TODO: EC keys may also suffer the same random problems in the future
privkey, _ = ecdsa.GenerateKey(elliptic.P256(), opts.nextReader()) privkey, _ = ecdsa.GenerateKey(elliptic.P256(), opts.nextReader())
} }
return privkey return privkey

View File

@ -103,10 +103,10 @@ func TestGenerateJWK(t *testing.T) {
switch key.(type) { switch key.(type) {
case *rsa.PrivateKey: case *rsa.PrivateKey:
// no-op // no-op
log.Println("is RSA") //log.Println("is RSA")
case *ecdsa.PrivateKey: case *ecdsa.PrivateKey:
// no-op // no-op
log.Println("is EC") //log.Println("is EC")
default: default:
t.Fatal(errors.New("impossible key type")) t.Fatal(errors.New("impossible key type"))
} }
@ -243,13 +243,104 @@ func TestGeneratePEM(t *testing.T) {
switch key.(type) { switch key.(type) {
case *rsa.PrivateKey: case *rsa.PrivateKey:
// no-op // no-op
log.Println("is RSA") //log.Println("is RSA")
case *ecdsa.PrivateKey: case *ecdsa.PrivateKey:
// no-op // 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: default:
t.Fatal(errors.New("impossible key type")) t.Fatal(errors.New("impossible key type"))
} }
//fmt.Printf("%#v\n", key)
} }

View File

@ -182,11 +182,13 @@ func Route(jwksPrefix string, privkey keypairs.PrivateKey) http.Handler {
}) })
http.HandleFunc("/private.jwk.json", api.GeneratePrivateJWK) http.HandleFunc("/private.jwk.json", api.GeneratePrivateJWK)
http.HandleFunc("/priv.der", api.GeneratePrivateDER) http.HandleFunc("/priv.der", api.GeneratePrivateDER)
http.HandleFunc("/priv.pem", api.GeneratePrivatePEM) 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) { http.HandleFunc("/inspect_token", func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization") token := r.Header.Get("Authorization")
log.Printf("%s %s %s\n", r.Method, r.URL.Path, token) log.Printf("%s %s %s\n", r.Method, r.URL.Path, token)

View File

@ -1,6 +1,7 @@
package xkeypairs package xkeypairs
import ( import (
"crypto"
"crypto/ecdsa" "crypto/ecdsa"
"crypto/rsa" "crypto/rsa"
"crypto/x509" "crypto/x509"
@ -14,6 +15,50 @@ import (
"git.rootprojects.org/root/keypairs" "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 // MarshalJWKPrivateKey outputs the given private key as JWK
func MarshalJWKPrivateKey(privkey keypairs.PrivateKey) []byte { func MarshalJWKPrivateKey(privkey keypairs.PrivateKey) []byte {
// thumbprint keys are alphabetically sorted and only include the necessary public parts // thumbprint keys are alphabetically sorted and only include the necessary public parts