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
|
// 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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue