keypairs/keyfetch/uncached/fetch.go

201 lines
4.5 KiB
Go

// Package uncached provides uncached versions of go-keypairs/keyfetch
package uncached
import (
"bytes"
"encoding/json"
"crypto/rsa"
"crypto/ecdsa"
"errors"
"io"
"io/ioutil"
"net"
"net/http"
"strings"
"time"
"git.rootprojects.org/root/keypairs"
)
// 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
// OIDCJWKs gets the OpenID Connect configuration from the baseURL and then calls JWKs with the specified jwks_uri
func OIDCJWKs(baseURL string) (JWKMapByID, PublicKeysMap, error) {
baseURL = normalizeBaseURL(baseURL)
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
func WellKnownJWKs(baseURL string) (JWKMapByID, PublicKeysMap, error) {
baseURL = normalizeBaseURL(baseURL)
url := baseURL + ".well-known/jwks.json"
return JWKs(url)
}
// JWKs fetches and parses a jwks.json (assuming well-known format)
func JWKs(jwksurl string) (JWKMapByID, PublicKeysMap, error) {
keys := PublicKeysMap{}
maps := JWKMapByID{}
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)
key, err := keypairs.NewJWKPublicKey(m)
if nil != err {
return nil, nil, err
}
keys[keypairs.Thumbprint(key.Key())] = key
maps[keypairs.Thumbprint(key.Key())] = m
}
return maps, keys, nil
}
// PEM fetches and parses a PEM (assuming well-known format)
func PEM(pemurl string) (map[string]string, keypairs.PublicKey, error) {
var pubd keypairs.PublicKeyDeprecated
if err := safeFetch(pemurl, func(body io.Reader) error {
pem, err := ioutil.ReadAll(body)
if nil != err {
return err
}
pubd, err = keypairs.ParsePublicKey(pem)
if nil != err {
return err
}
return nil
}); nil != err {
return nil, nil, err
}
jwk := map[string]interface{}{}
pub := pubd.Key()
body := bytes.NewBuffer(keypairs.MarshalJWKPublicKey(pub))
decoder := json.NewDecoder(body)
decoder.UseNumber()
_ = decoder.Decode(&jwk)
m := getStringMap(jwk)
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
default:
return nil, nil, errors.New("impossible key type")
}
return m, pub, nil
}
// Fetch retrieves a single JWK (plain, bare jwk) from a URL (off-spec)
func Fetch(url string) (map[string]string, keypairs.PublicKeyDeprecated, error) {
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,
}
var client = &http.Client{
Timeout: time.Second * 10,
Transport: netTransport,
}
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)
if nil != err {
return err
}
defer res.Body.Close()
return decoder(res.Body)
}
func normalizeBaseURL(iss string) string {
return strings.TrimRight(iss, "/") + "/"
}