2019-02-21 00:10:42 +00:00
|
|
|
// Package uncached provides uncached versions of go-keypairs/keyfetch
|
|
|
|
package uncached
|
|
|
|
|
|
|
|
import (
|
2019-03-15 23:52:53 +00:00
|
|
|
"bytes"
|
2019-02-21 00:10:42 +00:00
|
|
|
"encoding/json"
|
2020-10-21 09:16:34 +00:00
|
|
|
"crypto/rsa"
|
|
|
|
"crypto/ecdsa"
|
2019-03-15 23:52:53 +00:00
|
|
|
"errors"
|
2019-02-21 00:10:42 +00:00
|
|
|
"io"
|
2019-03-15 23:52:53 +00:00
|
|
|
"io/ioutil"
|
2019-02-21 00:10:42 +00:00
|
|
|
"net"
|
|
|
|
"net/http"
|
2019-02-22 21:40:46 +00:00
|
|
|
"strings"
|
2019-02-21 00:10:42 +00:00
|
|
|
"time"
|
|
|
|
|
2020-05-10 18:34:01 +00:00
|
|
|
"git.rootprojects.org/root/keypairs"
|
2019-02-21 00:10:42 +00:00
|
|
|
)
|
|
|
|
|
2020-10-21 09:16:34 +00:00
|
|
|
// URLishKey is TODO
|
|
|
|
var URLishKey = "_kid_url"
|
|
|
|
|
|
|
|
// JWKMapByID is TODO
|
|
|
|
type JWKMapByID = map[string]map[string]string
|
|
|
|
|
|
|
|
// PublicKeysMap is TODO
|
|
|
|
type PublicKeysMap = map[string]keypairs.PublicKeyDeprecated
|
|
|
|
|
2019-02-21 00:10:42 +00:00
|
|
|
// OIDCJWKs gets the OpenID Connect configuration from the baseURL and then calls JWKs with the specified jwks_uri
|
2020-10-21 09:16:34 +00:00
|
|
|
func OIDCJWKs(baseURL string) (JWKMapByID, PublicKeysMap, error) {
|
2019-02-22 21:40:46 +00:00
|
|
|
baseURL = normalizeBaseURL(baseURL)
|
2019-02-21 00:10:42 +00:00
|
|
|
oidcConf := struct {
|
|
|
|
JWKSURI string `json:"jwks_uri"`
|
|
|
|
}{}
|
|
|
|
|
|
|
|
// must come in as https://<domain>/
|
|
|
|
url := baseURL + ".well-known/openid-configuration"
|
|
|
|
err := safeFetch(url, func(body io.Reader) error {
|
|
|
|
decoder := json.NewDecoder(body)
|
|
|
|
decoder.UseNumber()
|
|
|
|
return decoder.Decode(&oidcConf)
|
|
|
|
})
|
|
|
|
if nil != err {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return JWKs(oidcConf.JWKSURI)
|
|
|
|
}
|
|
|
|
|
|
|
|
// WellKnownJWKs calls JWKs with baseURL + /.well-known/jwks.json as constructs the jwks_uri
|
2020-10-21 09:16:34 +00:00
|
|
|
func WellKnownJWKs(baseURL string) (JWKMapByID, PublicKeysMap, error) {
|
2019-02-22 21:40:46 +00:00
|
|
|
baseURL = normalizeBaseURL(baseURL)
|
2019-03-08 21:28:23 +00:00
|
|
|
url := baseURL + ".well-known/jwks.json"
|
2019-02-21 00:10:42 +00:00
|
|
|
|
2019-03-08 21:28:23 +00:00
|
|
|
return JWKs(url)
|
2019-02-21 00:10:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// JWKs fetches and parses a jwks.json (assuming well-known format)
|
2020-10-21 09:16:34 +00:00
|
|
|
func JWKs(jwksurl string) (JWKMapByID, PublicKeysMap, error) {
|
|
|
|
keys := PublicKeysMap{}
|
|
|
|
maps := JWKMapByID{}
|
2019-02-21 00:10:42 +00:00
|
|
|
resp := struct {
|
|
|
|
Keys []map[string]interface{} `json:"keys"`
|
|
|
|
}{
|
|
|
|
Keys: make([]map[string]interface{}, 0, 1),
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := safeFetch(jwksurl, func(body io.Reader) error {
|
|
|
|
decoder := json.NewDecoder(body)
|
|
|
|
decoder.UseNumber()
|
|
|
|
return decoder.Decode(&resp)
|
|
|
|
}); nil != err {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
for i := range resp.Keys {
|
|
|
|
k := resp.Keys[i]
|
|
|
|
m := getStringMap(k)
|
|
|
|
|
2020-04-10 17:59:44 +00:00
|
|
|
key, err := keypairs.NewJWKPublicKey(m)
|
|
|
|
|
|
|
|
if nil != err {
|
2019-02-21 00:10:42 +00:00
|
|
|
return nil, nil, err
|
|
|
|
}
|
2020-10-21 09:38:05 +00:00
|
|
|
keys[keypairs.Thumbprint(key.Key())] = key
|
|
|
|
maps[keypairs.Thumbprint(key.Key())] = m
|
2019-02-21 00:10:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return maps, keys, nil
|
|
|
|
}
|
|
|
|
|
2019-03-15 23:52:53 +00:00
|
|
|
// PEM fetches and parses a PEM (assuming well-known format)
|
2020-10-21 09:38:05 +00:00
|
|
|
func PEM(pemurl string) (map[string]string, keypairs.PublicKey, error) {
|
2020-10-21 09:16:34 +00:00
|
|
|
var pubd keypairs.PublicKeyDeprecated
|
2019-03-15 23:52:53 +00:00
|
|
|
if err := safeFetch(pemurl, func(body io.Reader) error {
|
|
|
|
pem, err := ioutil.ReadAll(body)
|
|
|
|
if nil != err {
|
|
|
|
return err
|
|
|
|
}
|
2020-10-21 09:16:34 +00:00
|
|
|
pubd, err = keypairs.ParsePublicKey(pem)
|
|
|
|
if nil != err {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
2019-03-15 23:52:53 +00:00
|
|
|
}); nil != err {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
jwk := map[string]interface{}{}
|
2020-10-21 09:38:05 +00:00
|
|
|
pub := pubd.Key()
|
2019-03-15 23:52:53 +00:00
|
|
|
body := bytes.NewBuffer(keypairs.MarshalJWKPublicKey(pub))
|
|
|
|
decoder := json.NewDecoder(body)
|
|
|
|
decoder.UseNumber()
|
|
|
|
_ = decoder.Decode(&jwk)
|
|
|
|
|
|
|
|
m := getStringMap(jwk)
|
2020-10-21 09:16:34 +00:00
|
|
|
m["kid"] = keypairs.Thumbprint(pub)
|
|
|
|
// TODO is this just junk?
|
|
|
|
m[URLishKey] = pemurl
|
|
|
|
|
|
|
|
switch pub.(type) {
|
|
|
|
case *ecdsa.PublicKey:
|
|
|
|
//p.KID = pemurl
|
|
|
|
case *rsa.PublicKey:
|
|
|
|
//p.KID = pemurl
|
2019-03-15 23:52:53 +00:00
|
|
|
default:
|
|
|
|
return nil, nil, errors.New("impossible key type")
|
|
|
|
}
|
|
|
|
|
|
|
|
return m, pub, nil
|
|
|
|
}
|
|
|
|
|
2019-02-21 00:10:42 +00:00
|
|
|
// Fetch retrieves a single JWK (plain, bare jwk) from a URL (off-spec)
|
2020-10-21 09:16:34 +00:00
|
|
|
func Fetch(url string) (map[string]string, keypairs.PublicKeyDeprecated, error) {
|
2019-02-21 00:10:42 +00:00
|
|
|
var m map[string]interface{}
|
|
|
|
if err := safeFetch(url, func(body io.Reader) error {
|
|
|
|
decoder := json.NewDecoder(body)
|
|
|
|
decoder.UseNumber()
|
|
|
|
return decoder.Decode(&m)
|
|
|
|
}); nil != err {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
n := getStringMap(m)
|
|
|
|
key, err := keypairs.NewJWKPublicKey(n)
|
|
|
|
if nil != err {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return n, key, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func getStringMap(m map[string]interface{}) map[string]string {
|
|
|
|
n := make(map[string]string)
|
|
|
|
|
|
|
|
// TODO get issuer from x5c, if exists
|
|
|
|
|
|
|
|
// convert map[string]interface{} to map[string]string
|
|
|
|
for j := range m {
|
|
|
|
switch s := m[j].(type) {
|
|
|
|
case string:
|
|
|
|
n[j] = s
|
|
|
|
default:
|
|
|
|
// safely ignore
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return n
|
|
|
|
}
|
|
|
|
|
|
|
|
type decodeFunc func(io.Reader) error
|
|
|
|
|
|
|
|
// TODO: also limit the body size
|
|
|
|
func safeFetch(url string, decoder decodeFunc) error {
|
|
|
|
var netTransport = &http.Transport{
|
|
|
|
Dial: (&net.Dialer{
|
|
|
|
Timeout: 5 * time.Second,
|
|
|
|
}).Dial,
|
|
|
|
TLSHandshakeTimeout: 5 * time.Second,
|
|
|
|
}
|
2019-03-08 21:28:23 +00:00
|
|
|
var client = &http.Client{
|
2019-02-21 00:10:42 +00:00
|
|
|
Timeout: time.Second * 10,
|
|
|
|
Transport: netTransport,
|
|
|
|
}
|
|
|
|
|
2019-03-08 21:28:23 +00:00
|
|
|
req, err := http.NewRequest("GET", url, nil)
|
|
|
|
req.Header.Set("User-Agent", "go-keypairs/keyfetch")
|
|
|
|
req.Header.Set("Accept", "application/json;q=0.9,*/*;q=0.8")
|
|
|
|
res, err := client.Do(req)
|
2019-02-21 00:10:42 +00:00
|
|
|
if nil != err {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer res.Body.Close()
|
|
|
|
|
|
|
|
return decoder(res.Body)
|
|
|
|
}
|
2019-02-22 21:40:46 +00:00
|
|
|
|
|
|
|
func normalizeBaseURL(iss string) string {
|
|
|
|
return strings.TrimRight(iss, "/") + "/"
|
|
|
|
}
|