add api and tests for generating public keys
This commit is contained in:
parent
d80c8226b5
commit
9b250c8cbb
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue