Compare commits
2 Commits
66e0639f48
...
285dc81dd7
Author | SHA1 | Date |
---|---|---|
AJ ONeal | 285dc81dd7 | |
AJ ONeal | 3ab579ad24 |
|
@ -1,4 +1,5 @@
|
||||||
{
|
{
|
||||||
|
"kty": "EC",
|
||||||
"crv": "P-256",
|
"crv": "P-256",
|
||||||
"d": "GYAwlBHc2mPsj1lp315HbYOmKNJ7esmO3JAkZVn9nJs",
|
"d": "GYAwlBHc2mPsj1lp315HbYOmKNJ7esmO3JAkZVn9nJs",
|
||||||
"x": "ToL2HppsTESXQKvp7ED6NMgV4YnwbMeONexNry3KDNQ",
|
"x": "ToL2HppsTESXQKvp7ED6NMgV4YnwbMeONexNry3KDNQ",
|
||||||
|
|
28
mockid.go
28
mockid.go
|
@ -1,7 +1,6 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
@ -12,6 +11,7 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"git.coolaj86.com/coolaj86/go-mockid/mockid"
|
"git.coolaj86.com/coolaj86/go-mockid/mockid"
|
||||||
|
"git.rootprojects.org/root/keypairs"
|
||||||
|
|
||||||
_ "github.com/joho/godotenv/autoload"
|
_ "github.com/joho/godotenv/autoload"
|
||||||
)
|
)
|
||||||
|
@ -44,24 +44,13 @@ func main() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
jwkm := map[string]string{}
|
privkey, err := keypairs.ParseJWKPrivateKey(jwkb)
|
||||||
err = json.Unmarshal(jwkb, &jwkm)
|
|
||||||
if nil != err {
|
if nil != err {
|
||||||
// TODO delete the bad file?
|
// TODO delete the bad file?
|
||||||
panic(fmt.Errorf("unmarshal jwk %v: %w", string(jwkb), err))
|
panic(fmt.Errorf("unmarshal jwk %v: %w", string(jwkb), err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
jwk := &mockid.PrivateJWK{
|
|
||||||
PublicJWK: mockid.PublicJWK{
|
|
||||||
Crv: jwkm["crv"],
|
|
||||||
X: jwkm["x"],
|
|
||||||
Y: jwkm["y"],
|
|
||||||
},
|
|
||||||
D: jwkm["d"],
|
|
||||||
}
|
|
||||||
priv := mockid.ParseKey(jwk)
|
|
||||||
|
|
||||||
if nil != urlFlag && "" != *urlFlag {
|
if nil != urlFlag && "" != *urlFlag {
|
||||||
host = *urlFlag
|
host = *urlFlag
|
||||||
} else {
|
} else {
|
||||||
|
@ -80,7 +69,7 @@ func main() {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
mockid.Route(jwksPrefix, priv, jwk)
|
mockid.Route(jwksPrefix, privkey)
|
||||||
|
|
||||||
fs := http.FileServer(http.Dir("./public"))
|
fs := http.FileServer(http.Dir("./public"))
|
||||||
http.Handle("/", fs)
|
http.Handle("/", fs)
|
||||||
|
@ -97,11 +86,12 @@ func main() {
|
||||||
done <- true
|
done <- true
|
||||||
}()
|
}()
|
||||||
|
|
||||||
b, _ := json.Marshal(jwk)
|
// TODO privB := keypairs.MarshalJWKPrivateKey(privkey)
|
||||||
fmt.Printf("Private Key:\n\t%s\n", string(b))
|
privB := mockid.MarshalJWKPrivateKey(privkey)
|
||||||
b, _ = json.Marshal(jwk.PublicJWK)
|
fmt.Printf("Private Key:\n\t%s\n", string(privB))
|
||||||
fmt.Printf("Public Key:\n\t%s\n", string(b))
|
pubB := keypairs.MarshalJWKPublicKey(keypairs.NewPublicKey(privkey.Public()))
|
||||||
protected, payload, token := mockid.GenToken(host, priv, url.Values{})
|
fmt.Printf("Public Key:\n\t%s\n", string(pubB))
|
||||||
|
protected, payload, token := mockid.GenToken(host, privkey, url.Values{})
|
||||||
fmt.Printf("Protected (Header):\n\t%s\n", protected)
|
fmt.Printf("Protected (Header):\n\t%s\n", protected)
|
||||||
fmt.Printf("Payload (Claims):\n\t%s\n", payload)
|
fmt.Printf("Payload (Claims):\n\t%s\n", payload)
|
||||||
fmt.Printf("Access Token:\n\t%s\n", token)
|
fmt.Printf("Access Token:\n\t%s\n", token)
|
||||||
|
|
189
mockid/mockid.go
189
mockid/mockid.go
|
@ -2,8 +2,8 @@ package mockid
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
"crypto/elliptic"
|
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"crypto/sha512"
|
"crypto/sha512"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
@ -21,13 +21,9 @@ import (
|
||||||
|
|
||||||
"git.rootprojects.org/root/keypairs"
|
"git.rootprojects.org/root/keypairs"
|
||||||
"git.rootprojects.org/root/keypairs/keyfetch"
|
"git.rootprojects.org/root/keypairs/keyfetch"
|
||||||
|
//jwt "github.com/dgrijalva/jwt-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PrivateJWK struct {
|
|
||||||
PublicJWK
|
|
||||||
D string `json:"d"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PublicJWK struct {
|
type PublicJWK struct {
|
||||||
Crv string `json:"crv"`
|
Crv string `json:"crv"`
|
||||||
KeyID string `json:"kid,omitempty"`
|
KeyID string `json:"kid,omitempty"`
|
||||||
|
@ -36,15 +32,34 @@ type PublicJWK struct {
|
||||||
Y string `json:"y"`
|
Y string `json:"y"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type InspectableToken struct {
|
||||||
|
Public keypairs.PublicKey `json:"public"`
|
||||||
|
Protected map[string]interface{} `json:"protected"`
|
||||||
|
Payload map[string]interface{} `json:"payload"`
|
||||||
|
Signature string `json:"signature"`
|
||||||
|
Verified bool `json:"verified"`
|
||||||
|
Errors []string `json:"errors"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *InspectableToken) MarshalJSON() ([]byte, error) {
|
||||||
|
pub := keypairs.MarshalJWKPublicKey(t.Public)
|
||||||
|
header, _ := json.Marshal(t.Protected)
|
||||||
|
payload, _ := json.Marshal(t.Payload)
|
||||||
|
errs, _ := json.Marshal(t.Errors)
|
||||||
|
return []byte(fmt.Sprintf(
|
||||||
|
`{"public":%s,"protected":%s,"payload":%s,"signature":%q,"verified":%t,"errors":%s}`,
|
||||||
|
pub, header, payload, t.Signature, t.Verified, errs,
|
||||||
|
)), nil
|
||||||
|
}
|
||||||
|
|
||||||
var nonces map[string]int64
|
var nonces map[string]int64
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
nonces = make(map[string]int64)
|
nonces = make(map[string]int64)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Route(jwksPrefix string, priv *ecdsa.PrivateKey, jwk *PrivateJWK) {
|
func Route(jwksPrefix string, privkey keypairs.PrivateKey) {
|
||||||
pub := &priv.PublicKey
|
pubkey := keypairs.NewPublicKey(privkey.Public())
|
||||||
thumbprint := thumbprintKey(pub)
|
|
||||||
|
|
||||||
http.HandleFunc("/api/new-nonce", func(w http.ResponseWriter, r *http.Request) {
|
http.HandleFunc("/api/new-nonce", func(w http.ResponseWriter, r *http.Request) {
|
||||||
baseURL := getBaseURL(r)
|
baseURL := getBaseURL(r)
|
||||||
|
@ -110,7 +125,7 @@ func Route(jwksPrefix string, priv *ecdsa.PrivateKey, jwk *PrivateJWK) {
|
||||||
|
|
||||||
http.HandleFunc("/access_token", func(w http.ResponseWriter, r *http.Request) {
|
http.HandleFunc("/access_token", func(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Printf("%s %s\n", r.Method, r.URL.Path)
|
log.Printf("%s %s\n", r.Method, r.URL.Path)
|
||||||
_, _, token := GenToken(getBaseURL(r), priv, r.URL.Query())
|
_, _, token := GenToken(getBaseURL(r), privkey, r.URL.Query())
|
||||||
fmt.Fprintf(w, token)
|
fmt.Fprintf(w, token)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -135,7 +150,7 @@ func Route(jwksPrefix string, priv *ecdsa.PrivateKey, jwk *PrivateJWK) {
|
||||||
|
|
||||||
parts := strings.Split(token, ".")
|
parts := strings.Split(token, ".")
|
||||||
if 3 != len(parts) {
|
if 3 != len(parts) {
|
||||||
http.Error(w, "Bad Format: token should be in the format of <protected-header>.<body>.<signature>", http.StatusBadRequest)
|
http.Error(w, "Bad Format: token should be in the format of <protected-header>.<payload>.<signature>", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
protected64 := parts[0]
|
protected64 := parts[0]
|
||||||
|
@ -149,7 +164,7 @@ func Route(jwksPrefix string, priv *ecdsa.PrivateKey, jwk *PrivateJWK) {
|
||||||
}
|
}
|
||||||
dataB, err := base64.RawURLEncoding.DecodeString(data64)
|
dataB, err := base64.RawURLEncoding.DecodeString(data64)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
http.Error(w, "Bad Format: token's body should be URL-safe base64 encoded", http.StatusBadRequest)
|
http.Error(w, "Bad Format: token's payload should be URL-safe base64 encoded", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// TODO verify signature
|
// TODO verify signature
|
||||||
|
@ -177,12 +192,12 @@ func Route(jwksPrefix string, priv *ecdsa.PrivateKey, jwk *PrivateJWK) {
|
||||||
data := map[string]interface{}{}
|
data := map[string]interface{}{}
|
||||||
err = json.Unmarshal(dataB, &data)
|
err = json.Unmarshal(dataB, &data)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
http.Error(w, "Bad Format: token's body should be URL-safe base64-encoded JSON", http.StatusBadRequest)
|
http.Error(w, "Bad Format: token's payload should be URL-safe base64-encoded JSON", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
iss, issOK := data["iss"].(string)
|
iss, issOK := data["iss"].(string)
|
||||||
if !jwkOK && !issOK {
|
if !jwkOK && !issOK {
|
||||||
errors = append(errors, "body.iss must exist to complement header.kid")
|
errors = append(errors, "payload.iss must exist to complement header.kid")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub, err := keyfetch.OIDCJWK(kid, iss)
|
pub, err := keyfetch.OIDCJWK(kid, iss)
|
||||||
|
@ -193,23 +208,16 @@ func Route(jwksPrefix string, priv *ecdsa.PrivateKey, jwk *PrivateJWK) {
|
||||||
fmt.Println("fetched pub key:")
|
fmt.Println("fetched pub key:")
|
||||||
fmt.Println(pub)
|
fmt.Println(pub)
|
||||||
|
|
||||||
inspected := struct {
|
inspected := &InspectableToken{
|
||||||
Public keypairs.PublicKey `json:"public"`
|
|
||||||
Protected map[string]interface{} `json:"protected"`
|
|
||||||
Body map[string]interface{} `json:"body"`
|
|
||||||
Signature string `json:"signature"`
|
|
||||||
Verified bool `json:"verified"`
|
|
||||||
Errors []string `json:"errors"`
|
|
||||||
}{
|
|
||||||
Public: pub,
|
Public: pub,
|
||||||
Protected: protected,
|
Protected: protected,
|
||||||
Body: data,
|
Payload: data,
|
||||||
Signature: signature64,
|
Signature: signature64,
|
||||||
Verified: false,
|
Verified: false,
|
||||||
Errors: errors,
|
Errors: errors,
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenB, err := json.Marshal(inspected)
|
tokenB, err := json.MarshalIndent(inspected, "", " ")
|
||||||
if nil != err {
|
if nil != err {
|
||||||
fmt.Println("couldn't serialize inpsected token:")
|
fmt.Println("couldn't serialize inpsected token:")
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
|
@ -239,13 +247,19 @@ func Route(jwksPrefix string, priv *ecdsa.PrivateKey, jwk *PrivateJWK) {
|
||||||
prefix = prefixes[0]
|
prefix = prefixes[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
_, _, token := GenToken(getBaseURL(r), priv, r.URL.Query())
|
_, _, token := GenToken(getBaseURL(r), privkey, r.URL.Query())
|
||||||
fmt.Fprintf(w, "%s: %s%s", header, prefix, token)
|
fmt.Fprintf(w, "%s: %s%s", header, prefix, token)
|
||||||
})
|
})
|
||||||
|
|
||||||
http.HandleFunc("/key.jwk.json", func(w http.ResponseWriter, r *http.Request) {
|
http.HandleFunc("/key.jwk.json", func(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Printf("%s %s", r.Method, r.URL.Path)
|
log.Printf("%s %s", r.Method, r.URL.Path)
|
||||||
fmt.Fprintf(w, `{ "kty": "EC" , "crv": %q , "d": %q , "x": %q , "y": %q , "ext": true , "key_ops": ["sign"] }`, jwk.Crv, jwk.D, jwk.X, jwk.Y)
|
jwk := string(MarshalJWKPrivateKey(privkey))
|
||||||
|
jwk = strings.Replace(jwk, `{"`, `{ "`, 1)
|
||||||
|
jwk = strings.Replace(jwk, `",`, `", `, -1)
|
||||||
|
jwk = jwk[0 : len(jwk)-1]
|
||||||
|
jwk = jwk + `, "ext": true , "key_ops": ["sign"] }`
|
||||||
|
// `{ "kty": "EC" , "crv": %q , "d": %q , "x": %q , "y": %q }`, jwk.Crv, jwk.D, jwk.X, jwk.Y
|
||||||
|
fmt.Fprintf(w, jwk)
|
||||||
})
|
})
|
||||||
|
|
||||||
http.HandleFunc("/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) {
|
http.HandleFunc("/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -262,10 +276,14 @@ func Route(jwksPrefix string, priv *ecdsa.PrivateKey, jwk *PrivateJWK) {
|
||||||
b, err := ioutil.ReadFile(filepath.Join(jwksPrefix, strings.ToLower(kid)+".jwk.json"))
|
b, err := ioutil.ReadFile(filepath.Join(jwksPrefix, strings.ToLower(kid)+".jwk.json"))
|
||||||
if nil != err {
|
if nil != err {
|
||||||
//http.Error(w, "Not Found", http.StatusNotFound)
|
//http.Error(w, "Not Found", http.StatusNotFound)
|
||||||
jwkstr := fmt.Sprintf(
|
exp := strconv.FormatInt(time.Now().Add(15*time.Minute).Unix(), 10)
|
||||||
`{ "keys": [ { "kty": "EC" , "crv": %q , "x": %q , "y": %q , "kid": %q , "ext": true , "key_ops": ["verify"] , "exp": %s } ] }`,
|
jwk := string(keypairs.MarshalJWKPublicKey(pubkey))
|
||||||
jwk.Crv, jwk.X, jwk.Y, thumbprint, strconv.FormatInt(time.Now().Add(15*time.Minute).Unix(), 10),
|
jwk = strings.Replace(jwk, `{"`, `{ "`, 1)
|
||||||
)
|
jwk = strings.Replace(jwk, `",`, `" ,`, -1)
|
||||||
|
jwk = jwk[0 : len(jwk)-1]
|
||||||
|
jwk = jwk + fmt.Sprintf(`, "ext": true , "key_ops": ["verify"], "exp": %s }`, exp)
|
||||||
|
// { "kty": "EC" , "crv": %q , "x": %q , "y": %q , "kid": %q , "ext": true , "key_ops": ["verify"] , "exp": %s }
|
||||||
|
jwkstr := fmt.Sprintf(`{ "keys": [ %s ] }`, jwk)
|
||||||
fmt.Println(jwkstr)
|
fmt.Println(jwkstr)
|
||||||
fmt.Fprintf(w, jwkstr)
|
fmt.Fprintf(w, jwkstr)
|
||||||
return
|
return
|
||||||
|
@ -462,9 +480,15 @@ func postRSA(jwksPrefix string, tok map[string]interface{}, w http.ResponseWrite
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func GenToken(host string, priv *ecdsa.PrivateKey, query url.Values) (string, string, string) {
|
func GenToken(host string, privkey keypairs.PrivateKey, query url.Values) (string, string, string) {
|
||||||
thumbprint := thumbprintKey(&priv.PublicKey)
|
thumbprint := keypairs.ThumbprintPublicKey(keypairs.NewPublicKey(privkey.Public()))
|
||||||
protected := fmt.Sprintf(`{"typ":"JWT","alg":"ES256","kid":"%s"}`, thumbprint)
|
// TODO keypairs.Alg(key)
|
||||||
|
alg := "ES256"
|
||||||
|
switch privkey.(type) {
|
||||||
|
case *rsa.PrivateKey:
|
||||||
|
alg = "RS256"
|
||||||
|
}
|
||||||
|
protected := fmt.Sprintf(`{"typ":"JWT","alg":%q,"kid":"%s"}`, alg, thumbprint)
|
||||||
protected64 := base64.RawURLEncoding.EncodeToString([]byte(protected))
|
protected64 := base64.RawURLEncoding.EncodeToString([]byte(protected))
|
||||||
|
|
||||||
exp, err := parseExp(query.Get("exp"))
|
exp, err := parseExp(query.Get("exp"))
|
||||||
|
@ -481,47 +505,33 @@ func GenToken(host string, priv *ecdsa.PrivateKey, query url.Values) (string, st
|
||||||
payload64 := base64.RawURLEncoding.EncodeToString([]byte(payload))
|
payload64 := base64.RawURLEncoding.EncodeToString([]byte(payload))
|
||||||
|
|
||||||
hash := sha256.Sum256([]byte(fmt.Sprintf(`%s.%s`, protected64, payload64)))
|
hash := sha256.Sum256([]byte(fmt.Sprintf(`%s.%s`, protected64, payload64)))
|
||||||
r, s, _ := ecdsa.Sign(rand.Reader, priv, hash[:])
|
sig := JOSESign(privkey, hash[:])
|
||||||
rb := r.Bytes()
|
sig64 := base64.RawURLEncoding.EncodeToString(sig)
|
||||||
for len(rb) < 32 {
|
|
||||||
rb = append([]byte{0}, rb...)
|
|
||||||
}
|
|
||||||
sb := s.Bytes()
|
|
||||||
for len(rb) < 32 {
|
|
||||||
sb = append([]byte{0}, sb...)
|
|
||||||
}
|
|
||||||
sig64 := base64.RawURLEncoding.EncodeToString(append(rb, sb...))
|
|
||||||
token := fmt.Sprintf("%s.%s.%s\n", protected64, payload64, sig64)
|
token := fmt.Sprintf("%s.%s.%s\n", protected64, payload64, sig64)
|
||||||
return protected, payload, token
|
return protected, payload, token
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseKey(jwk *PrivateJWK) *ecdsa.PrivateKey {
|
// TODO: move to keypairs
|
||||||
xb, _ := base64.RawURLEncoding.DecodeString(jwk.X)
|
|
||||||
xi := &big.Int{}
|
|
||||||
xi.SetBytes(xb)
|
|
||||||
yb, _ := base64.RawURLEncoding.DecodeString(jwk.Y)
|
|
||||||
yi := &big.Int{}
|
|
||||||
yi.SetBytes(yb)
|
|
||||||
pub := &ecdsa.PublicKey{
|
|
||||||
Curve: elliptic.P256(),
|
|
||||||
X: xi,
|
|
||||||
Y: yi,
|
|
||||||
}
|
|
||||||
|
|
||||||
db, _ := base64.RawURLEncoding.DecodeString(jwk.D)
|
func JOSESign(privkey keypairs.PrivateKey, hash []byte) []byte {
|
||||||
di := &big.Int{}
|
var sig []byte
|
||||||
di.SetBytes(db)
|
|
||||||
priv := &ecdsa.PrivateKey{
|
|
||||||
PublicKey: *pub,
|
|
||||||
D: di,
|
|
||||||
}
|
|
||||||
return priv
|
|
||||||
}
|
|
||||||
|
|
||||||
func thumbprintKey(pub *ecdsa.PublicKey) string {
|
switch k := privkey.(type) {
|
||||||
minpub := []byte(fmt.Sprintf(`{"crv":%q,"kty":"EC","x":%q,"y":%q}`, "P-256", pub.X, pub.Y))
|
case *rsa.PrivateKey:
|
||||||
sha := sha256.Sum256(minpub)
|
panic("TODO: implement rsa sign")
|
||||||
return base64.RawURLEncoding.EncodeToString(sha[:])
|
case *ecdsa.PrivateKey:
|
||||||
|
r, s, _ := ecdsa.Sign(rand.Reader, k, hash[:])
|
||||||
|
rb := r.Bytes()
|
||||||
|
for len(rb) < 32 {
|
||||||
|
rb = append([]byte{0}, rb...)
|
||||||
|
}
|
||||||
|
sb := s.Bytes()
|
||||||
|
for len(rb) < 32 {
|
||||||
|
sb = append([]byte{0}, sb...)
|
||||||
|
}
|
||||||
|
sig = append(rb, sb...)
|
||||||
|
}
|
||||||
|
return sig
|
||||||
}
|
}
|
||||||
|
|
||||||
func issueNonce(w http.ResponseWriter, r *http.Request) {
|
func issueNonce(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -567,3 +577,46 @@ func getBaseURL(r *http.Request) string {
|
||||||
r.Host,
|
r.Host,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
switch k := privkey.(type) {
|
||||||
|
case *rsa.PrivateKey:
|
||||||
|
return MarshalRSAPrivateKey(k)
|
||||||
|
case *ecdsa.PrivateKey:
|
||||||
|
return MarshalECPrivateKey(k)
|
||||||
|
default:
|
||||||
|
// this is unreachable because we know the types that we pass in
|
||||||
|
log.Printf("keytype: %t, %+v\n", privkey, privkey)
|
||||||
|
panic(keypairs.ErrInvalidPublicKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalECPrivateKey will output the given private key as JWK
|
||||||
|
func MarshalECPrivateKey(k *ecdsa.PrivateKey) []byte {
|
||||||
|
crv := k.Curve.Params().Name
|
||||||
|
d := base64.RawURLEncoding.EncodeToString(k.D.Bytes())
|
||||||
|
x := base64.RawURLEncoding.EncodeToString(k.X.Bytes())
|
||||||
|
y := base64.RawURLEncoding.EncodeToString(k.Y.Bytes())
|
||||||
|
return []byte(fmt.Sprintf(
|
||||||
|
`{"crv":%q,"d":%q,"kty":"EC","x":%q,"y":%q}`,
|
||||||
|
crv, d, x, y,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalRSAPrivateKey will output the given private key as JWK
|
||||||
|
func MarshalRSAPrivateKey(pk *rsa.PrivateKey) []byte {
|
||||||
|
e := base64.RawURLEncoding.EncodeToString(big.NewInt(int64(pk.E)).Bytes())
|
||||||
|
n := base64.RawURLEncoding.EncodeToString(pk.N.Bytes())
|
||||||
|
d := base64.RawURLEncoding.EncodeToString(pk.D.Bytes())
|
||||||
|
p := base64.RawURLEncoding.EncodeToString(pk.Primes[0].Bytes())
|
||||||
|
q := base64.RawURLEncoding.EncodeToString(pk.Primes[1].Bytes())
|
||||||
|
dp := base64.RawURLEncoding.EncodeToString(pk.Precomputed.Dp.Bytes())
|
||||||
|
dq := base64.RawURLEncoding.EncodeToString(pk.Precomputed.Dq.Bytes())
|
||||||
|
qi := base64.RawURLEncoding.EncodeToString(pk.Precomputed.Qinv.Bytes())
|
||||||
|
return []byte(fmt.Sprintf(
|
||||||
|
`{"d":%q,"dp":%q,"dq":%q,"e":%q,"kty":"RSA","n":%q,"p":%q,"q":%q,"qi":%q}`,
|
||||||
|
d, dp, dq, e, n, p, q, qi,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
The MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2018-2019 Big Squid, Inc
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
|
@ -0,0 +1,63 @@
|
||||||
|
# go-keypairs
|
||||||
|
|
||||||
|
JSON Web Key (JWK) support and type safety lightly placed over top of Go's `crypto/ecdsa` and `crypto/rsa`
|
||||||
|
|
||||||
|
Useful for JWT, JOSE, etc.
|
||||||
|
|
||||||
|
```go
|
||||||
|
key, err := keypairs.ParsePrivateKey(bytesForJWKOrPEMOrDER)
|
||||||
|
|
||||||
|
pub, err := keypairs.ParsePublicKey(bytesForJWKOrPEMOrDER)
|
||||||
|
|
||||||
|
jwk, err := keypairs.MarshalJWKPublicKey(pub, time.Now().Add(2 * time.Day))
|
||||||
|
|
||||||
|
kid, err := keypairs.ThumbprintPublicKey(pub)
|
||||||
|
```
|
||||||
|
|
||||||
|
# API Documentation
|
||||||
|
|
||||||
|
See <https://godoc.org/github.com/big-squid/go-keypairs>
|
||||||
|
|
||||||
|
# Philosophy
|
||||||
|
|
||||||
|
Go's standard library is great.
|
||||||
|
|
||||||
|
Go has _excellent_ crytography support and provides wonderful
|
||||||
|
primitives for dealing with them.
|
||||||
|
|
||||||
|
I prefer to stay as close to Go's `crypto` package as possible,
|
||||||
|
just adding a light touch for JWT support and type safety.
|
||||||
|
|
||||||
|
# Type Safety
|
||||||
|
|
||||||
|
`crypto.PublicKey` is a "marker interface", meaning that it is **not typesafe**!
|
||||||
|
|
||||||
|
`go-keypairs` defines `type keypairs.PrivateKey interface { Public() crypto.PublicKey }`,
|
||||||
|
which is implemented by `crypto/rsa` and `crypto/ecdsa`
|
||||||
|
(but not `crypto/dsa`, which we really don't care that much about).
|
||||||
|
|
||||||
|
Go1.15 will add `[PublicKey.Equal(crypto.PublicKey)](https://github.com/golang/go/issues/21704)`,
|
||||||
|
which will make it possible to remove the additional wrapper over `PublicKey`
|
||||||
|
and use an interface instead.
|
||||||
|
|
||||||
|
Since there are no common methods between `rsa.PublicKey` and `ecdsa.PublicKey`,
|
||||||
|
go-keypairs lightly wraps each to implement `Thumbprint() string` (part of the JOSE/JWK spec).
|
||||||
|
|
||||||
|
## JSON Web Key (JWK) as a "codec"
|
||||||
|
|
||||||
|
Although there are many, many ways that JWKs could be interpreted
|
||||||
|
(possibly why they haven't made it into the standard library), `go-keypairs`
|
||||||
|
follows the basic pattern of `encoding/x509` to `Parse` and `Marshal`
|
||||||
|
only the most basic and most meaningful parts of a key.
|
||||||
|
|
||||||
|
I highly recommend that you use `Thumbprint()` for `KeyID` you also
|
||||||
|
get the benefit of not losing information when encoding and decoding
|
||||||
|
between the ASN.1, x509, PEM, and JWK formats.
|
||||||
|
|
||||||
|
# LICENSE
|
||||||
|
|
||||||
|
Copyright (c) 2020-present AJ ONeal
|
||||||
|
Copyright (c) 2018-2019 Big Squid, Inc.
|
||||||
|
|
||||||
|
This work is licensed under the terms of the MIT license.
|
||||||
|
For a copy, see <https://opensource.org/licenses/MIT>.
|
|
@ -0,0 +1,40 @@
|
||||||
|
/*
|
||||||
|
Package keypairs complements Go's standard keypair-related packages
|
||||||
|
(encoding/pem, crypto/x509, crypto/rsa, crypto/ecdsa, crypto/elliptic)
|
||||||
|
with JWK encoding support and typesafe PrivateKey and PublicKey interfaces.
|
||||||
|
|
||||||
|
Basics
|
||||||
|
|
||||||
|
key, err := keypairs.ParsePrivateKey(bytesForJWKOrPEMOrDER)
|
||||||
|
|
||||||
|
pub, err := keypairs.ParsePublicKey(bytesForJWKOrPEMOrDER)
|
||||||
|
|
||||||
|
jwk, err := keypairs.MarshalJWKPublicKey(pub, time.Now().Add(2 * time.Day))
|
||||||
|
|
||||||
|
kid, err := keypairs.ThumbprintPublicKey(pub)
|
||||||
|
|
||||||
|
Convenience functions are available which will fetch keys
|
||||||
|
(or retrieve them from cache) via OIDC, .well-known/jwks.json, and direct urls.
|
||||||
|
All keys are cached by Thumbprint, as well as kid(@issuer), if available.
|
||||||
|
|
||||||
|
import "git.rootprojects.org/root/keypairs/keyfetch"
|
||||||
|
|
||||||
|
pubs, err := keyfetch.OIDCJWKs("https://example.com/")
|
||||||
|
pubs, err := keyfetch.OIDCJWK(ThumbOrKeyID, "https://example.com/")
|
||||||
|
|
||||||
|
pubs, err := keyfetch.WellKnownJWKs("https://example.com/")
|
||||||
|
pubs, err := keyfetch.WellKnownJWK(ThumbOrKeyID, "https://example.com/")
|
||||||
|
|
||||||
|
pubs, err := keyfetch.JWKs("https://example.com/path/to/jwks/")
|
||||||
|
pubs, err := keyfetch.JWK(ThumbOrKeyID, "https://example.com/path/to/jwks/")
|
||||||
|
|
||||||
|
// From URL
|
||||||
|
pub, err := keyfetch.Fetch("https://example.com/jwk.json")
|
||||||
|
|
||||||
|
// From Cache only
|
||||||
|
pub := keyfetch.Get(thumbprint, "https://example.com/jwk.json")
|
||||||
|
|
||||||
|
A non-caching version with the same capabilities is also available.
|
||||||
|
|
||||||
|
*/
|
||||||
|
package keypairs
|
|
@ -0,0 +1,3 @@
|
||||||
|
module git.rootprojects.org/root/keypairs
|
||||||
|
|
||||||
|
go 1.12
|
|
@ -0,0 +1,516 @@
|
||||||
|
// Package keyfetch retrieve and cache PublicKeys
|
||||||
|
// from OIDC (https://example.com/.well-known/openid-configuration)
|
||||||
|
// and Auth0 (https://example.com/.well-known/jwks.json)
|
||||||
|
// JWKs URLs and expires them when `exp` is reached
|
||||||
|
// (or a default expiry if the key does not provide one).
|
||||||
|
// It uses the keypairs package to Unmarshal the JWKs into their
|
||||||
|
// native types (with a very thin shim to provide the type safety
|
||||||
|
// that Go's crypto.PublicKey and crypto.PrivateKey interfaces lack).
|
||||||
|
package keyfetch
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.rootprojects.org/root/keypairs"
|
||||||
|
"git.rootprojects.org/root/keypairs/keyfetch/uncached"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO should be ErrInvalidJWKURL
|
||||||
|
|
||||||
|
// EInvalidJWKURL means that the url did not provide JWKs
|
||||||
|
var EInvalidJWKURL = errors.New("url does not lead to valid JWKs")
|
||||||
|
|
||||||
|
// KeyCache is an in-memory key cache
|
||||||
|
var KeyCache = map[string]CachableKey{}
|
||||||
|
|
||||||
|
// KeyCacheMux is used to guard the in-memory cache
|
||||||
|
var KeyCacheMux = sync.Mutex{}
|
||||||
|
|
||||||
|
// ErrInsecureDomain means that plain http was used where https was expected
|
||||||
|
var ErrInsecureDomain = errors.New("Whitelists should only allow secure URLs (i.e. https://). To allow unsecured private networking (i.e. Docker) pass PrivateWhitelist as a list of private URLs")
|
||||||
|
|
||||||
|
// TODO Cacheable key (shouldn't this be private)?
|
||||||
|
|
||||||
|
// CachableKey represents
|
||||||
|
type CachableKey struct {
|
||||||
|
Key keypairs.PublicKey
|
||||||
|
Expiry time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// maybe TODO use this poor-man's enum to allow kids thumbs to be accepted by the same method?
|
||||||
|
/*
|
||||||
|
type KeyID string
|
||||||
|
|
||||||
|
func (kid KeyID) ID() string {
|
||||||
|
return string(kid)
|
||||||
|
}
|
||||||
|
func (kid KeyID) isID() {}
|
||||||
|
|
||||||
|
type Thumbprint string
|
||||||
|
|
||||||
|
func (thumb Thumbprint) ID() string {
|
||||||
|
return string(thumb)
|
||||||
|
}
|
||||||
|
func (thumb Thumbprint) isID() {}
|
||||||
|
|
||||||
|
type ID interface {
|
||||||
|
ID() string
|
||||||
|
isID()
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// StaleTime defines when public keys should be renewed (15 minutes by default)
|
||||||
|
var StaleTime = 15 * time.Minute
|
||||||
|
|
||||||
|
// DefaultKeyDuration defines how long a key should be considered fresh (48 hours by default)
|
||||||
|
var DefaultKeyDuration = 48 * time.Hour
|
||||||
|
|
||||||
|
// MinimumKeyDuration defines the minimum time that a key will be cached (1 hour by default)
|
||||||
|
var MinimumKeyDuration = time.Hour
|
||||||
|
|
||||||
|
// MaximumKeyDuration defines the maximum time that a key will be cached (72 hours by default)
|
||||||
|
var MaximumKeyDuration = 72 * time.Hour
|
||||||
|
|
||||||
|
// PublicKeysMap is a newtype for a map of keypairs.PublicKey
|
||||||
|
type PublicKeysMap map[string]keypairs.PublicKey
|
||||||
|
|
||||||
|
// OIDCJWKs fetches baseURL + ".well-known/openid-configuration" and then fetches and returns the Public Keys.
|
||||||
|
func OIDCJWKs(baseURL string) (PublicKeysMap, error) {
|
||||||
|
maps, keys, err := uncached.OIDCJWKs(baseURL)
|
||||||
|
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cacheKeys(maps, keys, baseURL)
|
||||||
|
return keys, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// OIDCJWK fetches baseURL + ".well-known/openid-configuration" and then returns the key matching kid (or thumbprint)
|
||||||
|
func OIDCJWK(kidOrThumb, iss string) (keypairs.PublicKey, error) {
|
||||||
|
return immediateOneOrFetch(kidOrThumb, iss, uncached.OIDCJWKs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WellKnownJWKs fetches baseURL + ".well-known/jwks.json" and caches and returns the keys
|
||||||
|
func WellKnownJWKs(kidOrThumb, iss string) (PublicKeysMap, error) {
|
||||||
|
maps, keys, err := uncached.WellKnownJWKs(iss)
|
||||||
|
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cacheKeys(maps, keys, iss)
|
||||||
|
return keys, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// WellKnownJWK fetches baseURL + ".well-known/jwks.json" and returns the key matching kid (or thumbprint)
|
||||||
|
func WellKnownJWK(kidOrThumb, iss string) (keypairs.PublicKey, error) {
|
||||||
|
return immediateOneOrFetch(kidOrThumb, iss, uncached.WellKnownJWKs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// JWKs returns a map of keys identified by their thumbprint
|
||||||
|
// (since kid may or may not be present)
|
||||||
|
func JWKs(jwksurl string) (PublicKeysMap, error) {
|
||||||
|
maps, keys, err := uncached.JWKs(jwksurl)
|
||||||
|
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
iss := strings.Replace(jwksurl, ".well-known/jwks.json", "", 1)
|
||||||
|
cacheKeys(maps, keys, iss)
|
||||||
|
return keys, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// JWK tries to return a key from cache, falling back to the /.well-known/jwks.json of the issuer
|
||||||
|
func JWK(kidOrThumb, iss string) (keypairs.PublicKey, error) {
|
||||||
|
return immediateOneOrFetch(kidOrThumb, iss, uncached.JWKs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PEM tries to return a key from cache, falling back to the specified PEM url
|
||||||
|
func PEM(url string) (keypairs.PublicKey, error) {
|
||||||
|
// url is kid in this case
|
||||||
|
return immediateOneOrFetch(url, url, func(string) (map[string]map[string]string, map[string]keypairs.PublicKey, error) {
|
||||||
|
m, key, err := uncached.PEM(url)
|
||||||
|
if nil != err {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// put in a map, just for caching
|
||||||
|
maps := map[string]map[string]string{}
|
||||||
|
maps[key.Thumbprint()] = m
|
||||||
|
maps[url] = m
|
||||||
|
|
||||||
|
keys := map[string]keypairs.PublicKey{}
|
||||||
|
keys[key.Thumbprint()] = key
|
||||||
|
keys[url] = key
|
||||||
|
|
||||||
|
return maps, keys, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch returns a key from cache, falling back to an exact url as the "issuer"
|
||||||
|
func Fetch(url string) (keypairs.PublicKey, error) {
|
||||||
|
// url is kid in this case
|
||||||
|
return immediateOneOrFetch(url, url, func(string) (map[string]map[string]string, map[string]keypairs.PublicKey, error) {
|
||||||
|
m, key, err := uncached.Fetch(url)
|
||||||
|
if nil != err {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// put in a map, just for caching
|
||||||
|
maps := map[string]map[string]string{}
|
||||||
|
maps[key.Thumbprint()] = m
|
||||||
|
|
||||||
|
keys := map[string]keypairs.PublicKey{}
|
||||||
|
keys[key.Thumbprint()] = key
|
||||||
|
|
||||||
|
return maps, keys, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get retrieves a key from cache, or returns an error.
|
||||||
|
// The issuer string may be empty if using a thumbprint rather than a kid.
|
||||||
|
func Get(kidOrThumb, iss string) keypairs.PublicKey {
|
||||||
|
if pub := get(kidOrThumb, iss); nil != pub {
|
||||||
|
return pub.Key
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func get(kidOrThumb, iss string) *CachableKey {
|
||||||
|
iss = normalizeIssuer(iss)
|
||||||
|
KeyCacheMux.Lock()
|
||||||
|
defer KeyCacheMux.Unlock()
|
||||||
|
|
||||||
|
// we're safe to check the cache by kid alone
|
||||||
|
// by virtue that we never set it by kid alone
|
||||||
|
hit, ok := KeyCache[kidOrThumb]
|
||||||
|
if ok {
|
||||||
|
if now := time.Now(); hit.Expiry.Sub(now) > 0 {
|
||||||
|
// only return non-expired keys
|
||||||
|
return &hit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
id := kidOrThumb + "@" + iss
|
||||||
|
hit, ok = KeyCache[id]
|
||||||
|
if ok {
|
||||||
|
if now := time.Now(); hit.Expiry.Sub(now) > 0 {
|
||||||
|
// only return non-expired keys
|
||||||
|
return &hit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func immediateOneOrFetch(kidOrThumb, iss string, fetcher myfetcher) (keypairs.PublicKey, error) {
|
||||||
|
now := time.Now()
|
||||||
|
key := get(kidOrThumb, iss)
|
||||||
|
|
||||||
|
if nil == key {
|
||||||
|
return fetchAndSelect(kidOrThumb, iss, fetcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch just a little before the key actually expires
|
||||||
|
if key.Expiry.Sub(now) <= StaleTime {
|
||||||
|
go fetchAndSelect(kidOrThumb, iss, fetcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
return key.Key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type myfetcher func(string) (map[string]map[string]string, map[string]keypairs.PublicKey, error)
|
||||||
|
|
||||||
|
func fetchAndSelect(id, baseURL string, fetcher myfetcher) (keypairs.PublicKey, error) {
|
||||||
|
maps, keys, err := fetcher(baseURL)
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cacheKeys(maps, keys, baseURL)
|
||||||
|
|
||||||
|
for i := range keys {
|
||||||
|
key := keys[i]
|
||||||
|
|
||||||
|
if id == key.Thumbprint() {
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if id == key.KeyID() {
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("Key identified by '%s' was not found at %s", id, baseURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func cacheKeys(maps map[string]map[string]string, keys map[string]keypairs.PublicKey, issuer string) {
|
||||||
|
for i := range keys {
|
||||||
|
key := keys[i]
|
||||||
|
m := maps[i]
|
||||||
|
iss := issuer
|
||||||
|
if "" != m["iss"] {
|
||||||
|
iss = m["iss"]
|
||||||
|
}
|
||||||
|
iss = normalizeIssuer(iss)
|
||||||
|
cacheKey(m["kid"], iss, m["exp"], key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cacheKey(kid, iss, expstr string, pub keypairs.PublicKey) error {
|
||||||
|
var expiry time.Time
|
||||||
|
iss = normalizeIssuer(iss)
|
||||||
|
|
||||||
|
exp, _ := strconv.ParseInt(expstr, 10, 64)
|
||||||
|
if 0 == exp {
|
||||||
|
// use default
|
||||||
|
expiry = time.Now().Add(DefaultKeyDuration)
|
||||||
|
} else if exp < time.Now().Add(MinimumKeyDuration).Unix() || exp > time.Now().Add(MaximumKeyDuration).Unix() {
|
||||||
|
// use at least one hour
|
||||||
|
expiry = time.Now().Add(MinimumKeyDuration)
|
||||||
|
} else {
|
||||||
|
expiry = time.Unix(exp, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyCacheMux.Lock()
|
||||||
|
defer KeyCacheMux.Unlock()
|
||||||
|
// Put the key in the cache by both kid and thumbprint, and set the expiry
|
||||||
|
id := kid + "@" + iss
|
||||||
|
KeyCache[id] = CachableKey{
|
||||||
|
Key: pub,
|
||||||
|
Expiry: expiry,
|
||||||
|
}
|
||||||
|
// Since thumbprints are crypto secure, iss isn't needed
|
||||||
|
thumb := pub.Thumbprint()
|
||||||
|
KeyCache[thumb] = CachableKey{
|
||||||
|
Key: pub,
|
||||||
|
Expiry: expiry,
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func clear() {
|
||||||
|
KeyCacheMux.Lock()
|
||||||
|
defer KeyCacheMux.Unlock()
|
||||||
|
KeyCache = map[string]CachableKey{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeIssuer(iss string) string {
|
||||||
|
return strings.TrimRight(iss, "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
func isTrustedIssuer(iss string, whitelist Whitelist, rs ...*http.Request) bool {
|
||||||
|
if "" == iss {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize the http:// and https:// and parse
|
||||||
|
iss = strings.TrimRight(iss, "/") + "/"
|
||||||
|
if strings.HasPrefix(iss, "http://") {
|
||||||
|
// ignore
|
||||||
|
} else if strings.HasPrefix(iss, "//") {
|
||||||
|
return false // TODO
|
||||||
|
} else if !strings.HasPrefix(iss, "https://") {
|
||||||
|
iss = "https://" + iss
|
||||||
|
}
|
||||||
|
issURL, err := url.Parse(iss)
|
||||||
|
if nil != err {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that
|
||||||
|
// * schemes match (https: == https:)
|
||||||
|
// * paths match (/foo/ == /foo/, always with trailing slash added)
|
||||||
|
// * hostnames are compatible (a == b or "sub.foo.com".HasSufix(".foo.com"))
|
||||||
|
for i := range []*url.URL(whitelist) {
|
||||||
|
u := whitelist[i]
|
||||||
|
|
||||||
|
if issURL.Scheme != u.Scheme {
|
||||||
|
continue
|
||||||
|
} else if u.Path != strings.TrimRight(issURL.Path, "/")+"/" {
|
||||||
|
continue
|
||||||
|
} else if issURL.Host != u.Host {
|
||||||
|
if '.' == u.Host[0] && strings.HasSuffix(issURL.Host, u.Host) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// All failures have been handled
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if implicit issuer is available
|
||||||
|
if 0 == len(rs) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return hasImplicitTrust(issURL, rs[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// hasImplicitTrust relies on the security of DNS and TLS to determine if the
|
||||||
|
// headers of the request can be trusted as identifying the server itself as
|
||||||
|
// a valid issuer, without additional configuration.
|
||||||
|
//
|
||||||
|
// Helpful for testing, but in the wrong hands could easily lead to a zero-day.
|
||||||
|
func hasImplicitTrust(issURL *url.URL, r *http.Request) bool {
|
||||||
|
if nil == r {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanity check that, if a load balancer exists, it isn't misconfigured
|
||||||
|
proto := r.Header.Get("X-Forwarded-Proto")
|
||||||
|
if "" != proto && proto != "https" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the host
|
||||||
|
// * If TLS, block Domain Fronting
|
||||||
|
// * Otherwise assume trusted proxy
|
||||||
|
// * Otherwise assume test environment
|
||||||
|
var host string
|
||||||
|
if nil != r.TLS {
|
||||||
|
// Note that if this were to be implemented for HTTP/2 it would need to
|
||||||
|
// check all names on the certificate, not just the one with which the
|
||||||
|
// original connection was established. However, not our problem here.
|
||||||
|
// See https://serverfault.com/a/908087/93930
|
||||||
|
if r.TLS.ServerName != r.Host {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
host = r.Host
|
||||||
|
} else {
|
||||||
|
host = r.Header.Get("X-Forwarded-Host")
|
||||||
|
if "" == host {
|
||||||
|
host = r.Host
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same tests as above, adjusted since it can't handle wildcards and, since
|
||||||
|
// the path is variable, we make the assumption that a child can trust a
|
||||||
|
// parent, but that a parent cannot trust a child.
|
||||||
|
if r.Host != issURL.Host {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(strings.TrimRight(r.URL.Path, "/")+"/", issURL.Path) {
|
||||||
|
// Ex: Request URL Token Issuer
|
||||||
|
// !"https:example.com/johndoe/api/dothing".HasPrefix("https:example.com/")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whitelist is a newtype for an array of URLs
|
||||||
|
type Whitelist []*url.URL
|
||||||
|
|
||||||
|
// NewWhitelist turns an array of URLs (such as https://example.com/) into
|
||||||
|
// a parsed array of *url.URLs that can be used by the IsTrustedIssuer function
|
||||||
|
func NewWhitelist(issuers []string, privateList ...[]string) (Whitelist, error) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
list := []*url.URL{}
|
||||||
|
if 0 != len(issuers) {
|
||||||
|
insecure := false
|
||||||
|
list, err = newWhitelist(list, issuers, insecure)
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if 0 != len(privateList) && 0 != len(privateList[0]) {
|
||||||
|
insecure := true
|
||||||
|
list, err = newWhitelist(list, privateList[0], insecure)
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Whitelist(list), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newWhitelist(list []*url.URL, issuers []string, insecure bool) (Whitelist, error) {
|
||||||
|
for i := range issuers {
|
||||||
|
iss := issuers[i]
|
||||||
|
if "" == strings.TrimSpace(iss) {
|
||||||
|
fmt.Println("[Warning] You have an empty string in your keyfetch whitelist.")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should have a valid http or https prefix
|
||||||
|
// TODO support custom prefixes (i.e. app://) ?
|
||||||
|
if strings.HasPrefix(iss, "http://") {
|
||||||
|
if !insecure {
|
||||||
|
log.Println("Oops! You have an insecure domain in your whitelist: ", iss)
|
||||||
|
return nil, ErrInsecureDomain
|
||||||
|
}
|
||||||
|
} else if strings.HasPrefix(iss, "//") {
|
||||||
|
// TODO
|
||||||
|
return nil, errors.New("Rather than prefixing with // to support multiple protocols, add them seperately:" + iss)
|
||||||
|
} else if !strings.HasPrefix(iss, "https://") {
|
||||||
|
iss = "https://" + iss
|
||||||
|
}
|
||||||
|
|
||||||
|
// trailing slash as a boundary character, which may or may not denote a directory
|
||||||
|
iss = strings.TrimRight(iss, "/") + "/"
|
||||||
|
u, err := url.Parse(iss)
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip any * prefix, for easier comparison later
|
||||||
|
// *.example.com => .example.com
|
||||||
|
if strings.HasPrefix(u.Host, "*.") {
|
||||||
|
u.Host = u.Host[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
list = append(list, u)
|
||||||
|
}
|
||||||
|
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
IsTrustedIssuer returns true when the `iss` (i.e. from a token) matches one
|
||||||
|
in the provided whitelist (also matches wildcard domains).
|
||||||
|
|
||||||
|
You may explicitly allow insecure http (i.e. for automated testing) by
|
||||||
|
including http:// Otherwise the scheme in each item of the whitelist should
|
||||||
|
include the "https://" prefix.
|
||||||
|
|
||||||
|
SECURITY CONSIDERATIONS (Please Read)
|
||||||
|
|
||||||
|
You'll notice that *http.Request is optional. It should only be used under these
|
||||||
|
three circumstances:
|
||||||
|
|
||||||
|
1) Something else guarantees http -> https redirection happens before the
|
||||||
|
connection gets here AND this server directly handles TLS/SSL.
|
||||||
|
|
||||||
|
2) If you're using a load balancer or web server, and this doesn't handle
|
||||||
|
TLS/SSL directly, that server is _explicitly_ configured to protect
|
||||||
|
against Domain Fronting attacks. As of 2019, most web servers and load
|
||||||
|
balancers do not protect against that by default.
|
||||||
|
|
||||||
|
3) If you only use it to make your automated integration testing more
|
||||||
|
and it isn't enabled in production.
|
||||||
|
|
||||||
|
Otherwise, DO NOT pass in *http.Request as you will introduce a 0-day
|
||||||
|
vulnerability allowing an attacker to spoof any token issuer of their choice.
|
||||||
|
The only reason I allowed this in a public library where non-experts would
|
||||||
|
encounter it is to make testing easier.
|
||||||
|
*/
|
||||||
|
func (w Whitelist) IsTrustedIssuer(iss string, rs ...*http.Request) bool {
|
||||||
|
return isTrustedIssuer(iss, w, rs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String will generate a space-delimited list of whitelisted URLs
|
||||||
|
func (w Whitelist) String() string {
|
||||||
|
s := []string{}
|
||||||
|
for i := range w {
|
||||||
|
s = append(s, w[i].String())
|
||||||
|
}
|
||||||
|
return strings.Join(s, " ")
|
||||||
|
}
|
183
vendor/git.rootprojects.org/root/keypairs/keyfetch/uncached/fetch.go
generated
vendored
Normal file
183
vendor/git.rootprojects.org/root/keypairs/keyfetch/uncached/fetch.go
generated
vendored
Normal file
|
@ -0,0 +1,183 @@
|
||||||
|
// Package uncached provides uncached versions of go-keypairs/keyfetch
|
||||||
|
package uncached
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.rootprojects.org/root/keypairs"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OIDCJWKs gets the OpenID Connect configuration from the baseURL and then calls JWKs with the specified jwks_uri
|
||||||
|
func OIDCJWKs(baseURL string) (map[string]map[string]string, map[string]keypairs.PublicKey, 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) (map[string]map[string]string, map[string]keypairs.PublicKey, 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) (map[string]map[string]string, map[string]keypairs.PublicKey, error) {
|
||||||
|
keys := map[string]keypairs.PublicKey{}
|
||||||
|
maps := map[string]map[string]string{}
|
||||||
|
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[key.Thumbprint()] = key
|
||||||
|
maps[key.Thumbprint()] = 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 pub keypairs.PublicKey
|
||||||
|
if err := safeFetch(pemurl, func(body io.Reader) error {
|
||||||
|
pem, err := ioutil.ReadAll(body)
|
||||||
|
if nil != err {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pub, err = keypairs.ParsePublicKey(pem)
|
||||||
|
return err
|
||||||
|
}); nil != err {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
jwk := map[string]interface{}{}
|
||||||
|
body := bytes.NewBuffer(keypairs.MarshalJWKPublicKey(pub))
|
||||||
|
decoder := json.NewDecoder(body)
|
||||||
|
decoder.UseNumber()
|
||||||
|
_ = decoder.Decode(&jwk)
|
||||||
|
|
||||||
|
m := getStringMap(jwk)
|
||||||
|
m["kid"] = pemurl
|
||||||
|
|
||||||
|
switch p := pub.(type) {
|
||||||
|
case *keypairs.ECPublicKey:
|
||||||
|
p.KID = pemurl
|
||||||
|
case *keypairs.RSAPublicKey:
|
||||||
|
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.PublicKey, 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, "/") + "/"
|
||||||
|
}
|
|
@ -0,0 +1,645 @@
|
||||||
|
package keypairs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto"
|
||||||
|
"crypto/dsa"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"math/big"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrInvalidPrivateKey means that the key is not a valid Private Key
|
||||||
|
var ErrInvalidPrivateKey = errors.New("PrivateKey must be of type *rsa.PrivateKey or *ecdsa.PrivateKey")
|
||||||
|
|
||||||
|
// ErrInvalidPublicKey means that the key is not a valid Public Key
|
||||||
|
var ErrInvalidPublicKey = errors.New("PublicKey must be of type *rsa.PublicKey or *ecdsa.PublicKey")
|
||||||
|
|
||||||
|
// ErrParsePublicKey means that the bytes cannot be parsed in any known format
|
||||||
|
var ErrParsePublicKey = errors.New("PublicKey bytes could not be parsed as PEM or DER (PKIX/SPKI, PKCS1, or X509 Certificate) or JWK")
|
||||||
|
|
||||||
|
// ErrParsePrivateKey means that the bytes cannot be parsed in any known format
|
||||||
|
var ErrParsePrivateKey = errors.New("PrivateKey bytes could not be parsed as PEM or DER (PKCS8, SEC1, or PKCS1) or JWK")
|
||||||
|
|
||||||
|
// ErrParseJWK means that the JWK is valid JSON but not a valid JWK
|
||||||
|
var ErrParseJWK = errors.New("JWK is missing required base64-encoded JSON fields")
|
||||||
|
|
||||||
|
// ErrInvalidKeyType means that the key is not an acceptable type
|
||||||
|
var ErrInvalidKeyType = errors.New("The JWK's 'kty' must be either 'RSA' or 'EC'")
|
||||||
|
|
||||||
|
// ErrInvalidCurve means that a non-standard curve was used
|
||||||
|
var ErrInvalidCurve = errors.New("The JWK's 'crv' must be either of the NIST standards 'P-256' or 'P-384'")
|
||||||
|
|
||||||
|
// ErrUnexpectedPublicKey means that a Private Key was expected
|
||||||
|
var ErrUnexpectedPublicKey = errors.New("PrivateKey was given where PublicKey was expected")
|
||||||
|
|
||||||
|
// ErrUnexpectedPrivateKey means that a Public Key was expected
|
||||||
|
var ErrUnexpectedPrivateKey = errors.New("PublicKey was given where PrivateKey was expected")
|
||||||
|
|
||||||
|
// ErrDevSwapPrivatePublic means that the developer compiled bad code that swapped public and private keys
|
||||||
|
const ErrDevSwapPrivatePublic = "[Developer Error] You passed either crypto.PrivateKey or crypto.PublicKey where the other was expected."
|
||||||
|
|
||||||
|
// ErrDevBadKeyType means that the developer compiled bad code that passes the wrong type
|
||||||
|
const ErrDevBadKeyType = "[Developer Error] crypto.PublicKey and crypto.PrivateKey are somewhat deceptive. They're actually empty interfaces that accept any object, even non-crypto objects. You passed an object of type '%T' by mistake."
|
||||||
|
|
||||||
|
// PrivateKey is a zero-cost typesafe substitue for crypto.PrivateKey
|
||||||
|
type PrivateKey interface {
|
||||||
|
Public() crypto.PublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublicKey thinly veils crypto.PublicKey for type safety
|
||||||
|
type PublicKey interface {
|
||||||
|
crypto.PublicKey
|
||||||
|
Thumbprint() string
|
||||||
|
KeyID() string
|
||||||
|
Key() crypto.PublicKey
|
||||||
|
ExpiresAt() time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// ECPublicKey adds common methods to *ecdsa.PublicKey for type safety
|
||||||
|
type ECPublicKey struct {
|
||||||
|
PublicKey *ecdsa.PublicKey // empty interface
|
||||||
|
KID string
|
||||||
|
Expiry time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// RSAPublicKey adds common methods to *rsa.PublicKey for type safety
|
||||||
|
type RSAPublicKey struct {
|
||||||
|
PublicKey *rsa.PublicKey // empty interface
|
||||||
|
KID string
|
||||||
|
Expiry time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thumbprint returns a JWK thumbprint. See https://stackoverflow.com/questions/42588786/how-to-fingerprint-a-jwk
|
||||||
|
func (p *ECPublicKey) Thumbprint() string {
|
||||||
|
return ThumbprintUntypedPublicKey(p.PublicKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyID returns the JWK `kid`, which will be the Thumbprint for keys generated with this library
|
||||||
|
func (p *ECPublicKey) KeyID() string {
|
||||||
|
return p.KID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Key returns the PublicKey
|
||||||
|
func (p *ECPublicKey) Key() crypto.PublicKey {
|
||||||
|
return p.PublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpireAt sets the time at which this Public Key should be considered invalid
|
||||||
|
func (p *ECPublicKey) ExpireAt(t time.Time) {
|
||||||
|
p.Expiry = t
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpiresAt gets the time at which this Public Key should be considered invalid
|
||||||
|
func (p *ECPublicKey) ExpiresAt() time.Time {
|
||||||
|
return p.Expiry
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thumbprint returns a JWK thumbprint. See https://stackoverflow.com/questions/42588786/how-to-fingerprint-a-jwk
|
||||||
|
func (p *RSAPublicKey) Thumbprint() string {
|
||||||
|
return ThumbprintUntypedPublicKey(p.PublicKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyID returns the JWK `kid`, which will be the Thumbprint for keys generated with this library
|
||||||
|
func (p *RSAPublicKey) KeyID() string {
|
||||||
|
return p.KID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Key returns the PublicKey
|
||||||
|
func (p *RSAPublicKey) Key() crypto.PublicKey {
|
||||||
|
return p.PublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpireAt sets the time at which this Public Key should be considered invalid
|
||||||
|
func (p *RSAPublicKey) ExpireAt(t time.Time) {
|
||||||
|
p.Expiry = t
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpiresAt gets the time at which this Public Key should be considered invalid
|
||||||
|
func (p *RSAPublicKey) ExpiresAt() time.Time {
|
||||||
|
return p.Expiry
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPublicKey wraps a crypto.PublicKey to make it typesafe.
|
||||||
|
func NewPublicKey(pub crypto.PublicKey, kid ...string) PublicKey {
|
||||||
|
var k PublicKey
|
||||||
|
switch p := pub.(type) {
|
||||||
|
case *ecdsa.PublicKey:
|
||||||
|
eckey := &ECPublicKey{
|
||||||
|
PublicKey: p,
|
||||||
|
}
|
||||||
|
if 0 != len(kid) {
|
||||||
|
eckey.KID = kid[0]
|
||||||
|
} else {
|
||||||
|
eckey.KID = ThumbprintECPublicKey(p)
|
||||||
|
}
|
||||||
|
k = eckey
|
||||||
|
case *rsa.PublicKey:
|
||||||
|
rsakey := &RSAPublicKey{
|
||||||
|
PublicKey: p,
|
||||||
|
}
|
||||||
|
if 0 != len(kid) {
|
||||||
|
rsakey.KID = kid[0]
|
||||||
|
} else {
|
||||||
|
rsakey.KID = ThumbprintRSAPublicKey(p)
|
||||||
|
}
|
||||||
|
k = rsakey
|
||||||
|
case *ecdsa.PrivateKey:
|
||||||
|
panic(errors.New(ErrDevSwapPrivatePublic))
|
||||||
|
case *rsa.PrivateKey:
|
||||||
|
panic(errors.New(ErrDevSwapPrivatePublic))
|
||||||
|
case *dsa.PublicKey:
|
||||||
|
panic(ErrInvalidPublicKey)
|
||||||
|
case *dsa.PrivateKey:
|
||||||
|
panic(ErrInvalidPrivateKey)
|
||||||
|
default:
|
||||||
|
panic(fmt.Errorf(ErrDevBadKeyType, pub))
|
||||||
|
}
|
||||||
|
|
||||||
|
return k
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJWKPublicKey outputs a JWK with its key id (kid) and an optional expiration,
|
||||||
|
// making it suitable for use as an OIDC public key.
|
||||||
|
func MarshalJWKPublicKey(key PublicKey, exp ...time.Time) []byte {
|
||||||
|
// thumbprint keys are alphabetically sorted and only include the necessary public parts
|
||||||
|
switch k := key.Key().(type) {
|
||||||
|
case *rsa.PublicKey:
|
||||||
|
return MarshalRSAPublicKey(k, exp...)
|
||||||
|
case *ecdsa.PublicKey:
|
||||||
|
return MarshalECPublicKey(k, exp...)
|
||||||
|
case *dsa.PublicKey:
|
||||||
|
panic(ErrInvalidPublicKey)
|
||||||
|
default:
|
||||||
|
// this is unreachable because we know the types that we pass in
|
||||||
|
log.Printf("keytype: %t, %+v\n", key, key)
|
||||||
|
panic(ErrInvalidPublicKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ThumbprintPublicKey returns the SHA256 RFC-spec JWK thumbprint
|
||||||
|
func ThumbprintPublicKey(pub PublicKey) string {
|
||||||
|
return ThumbprintUntypedPublicKey(pub.Key())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ThumbprintUntypedPublicKey is a non-typesafe version of ThumbprintPublicKey
|
||||||
|
// (but will still panic, to help you discover bugs in development rather than production).
|
||||||
|
func ThumbprintUntypedPublicKey(pub crypto.PublicKey) string {
|
||||||
|
switch p := pub.(type) {
|
||||||
|
case PublicKey:
|
||||||
|
return ThumbprintUntypedPublicKey(p.Key())
|
||||||
|
case *ecdsa.PublicKey:
|
||||||
|
return ThumbprintECPublicKey(p)
|
||||||
|
case *rsa.PublicKey:
|
||||||
|
return ThumbprintRSAPublicKey(p)
|
||||||
|
default:
|
||||||
|
panic(ErrInvalidPublicKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalECPublicKey will take an EC key and output a JWK, with optional expiration date
|
||||||
|
func MarshalECPublicKey(k *ecdsa.PublicKey, exp ...time.Time) []byte {
|
||||||
|
thumb := ThumbprintECPublicKey(k)
|
||||||
|
crv := k.Curve.Params().Name
|
||||||
|
x := base64.RawURLEncoding.EncodeToString(k.X.Bytes())
|
||||||
|
y := base64.RawURLEncoding.EncodeToString(k.Y.Bytes())
|
||||||
|
expstr := ""
|
||||||
|
if 0 != len(exp) {
|
||||||
|
expstr = fmt.Sprintf(`"exp":%d,`, exp[0].Unix())
|
||||||
|
}
|
||||||
|
return []byte(fmt.Sprintf(`{"kid":%q,"use":"sig",%s"crv":%q,"kty":"EC","x":%q,"y":%q}`, thumb, expstr, crv, x, y))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalECPublicKeyWithoutKeyID will output the most minimal version of an EC JWK (no key id, no "use" flag, nada)
|
||||||
|
func MarshalECPublicKeyWithoutKeyID(k *ecdsa.PublicKey) []byte {
|
||||||
|
crv := k.Curve.Params().Name
|
||||||
|
x := base64.RawURLEncoding.EncodeToString(k.X.Bytes())
|
||||||
|
y := base64.RawURLEncoding.EncodeToString(k.Y.Bytes())
|
||||||
|
return []byte(fmt.Sprintf(`{"crv":%q,"kty":"EC","x":%q,"y":%q}`, crv, x, y))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ThumbprintECPublicKey will output a RFC-spec SHA256 JWK thumbprint of an EC public key
|
||||||
|
func ThumbprintECPublicKey(k *ecdsa.PublicKey) string {
|
||||||
|
thumbprintable := MarshalECPublicKeyWithoutKeyID(k)
|
||||||
|
sha := sha256.Sum256(thumbprintable)
|
||||||
|
return base64.RawURLEncoding.EncodeToString(sha[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalRSAPublicKey will take an RSA key and output a JWK, with optional expiration date
|
||||||
|
func MarshalRSAPublicKey(p *rsa.PublicKey, exp ...time.Time) []byte {
|
||||||
|
thumb := ThumbprintRSAPublicKey(p)
|
||||||
|
e := base64.RawURLEncoding.EncodeToString(big.NewInt(int64(p.E)).Bytes())
|
||||||
|
n := base64.RawURLEncoding.EncodeToString(p.N.Bytes())
|
||||||
|
expstr := ""
|
||||||
|
if 0 != len(exp) {
|
||||||
|
expstr = fmt.Sprintf(`"exp":%d,`, exp[0].Unix())
|
||||||
|
}
|
||||||
|
return []byte(fmt.Sprintf(`{"kid":%q,"use":"sig",%s"e":%q,"kty":"RSA","n":%q}`, thumb, expstr, e, n))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalRSAPublicKeyWithoutKeyID will output the most minimal version of an RSA JWK (no key id, no "use" flag, nada)
|
||||||
|
func MarshalRSAPublicKeyWithoutKeyID(p *rsa.PublicKey) []byte {
|
||||||
|
e := base64.RawURLEncoding.EncodeToString(big.NewInt(int64(p.E)).Bytes())
|
||||||
|
n := base64.RawURLEncoding.EncodeToString(p.N.Bytes())
|
||||||
|
return []byte(fmt.Sprintf(`{"e":%q,"kty":"RSA","n":%q}`, e, n))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ThumbprintRSAPublicKey will output a RFC-spec SHA256 JWK thumbprint of an EC public key
|
||||||
|
func ThumbprintRSAPublicKey(p *rsa.PublicKey) string {
|
||||||
|
thumbprintable := MarshalRSAPublicKeyWithoutKeyID(p)
|
||||||
|
sha := sha256.Sum256([]byte(thumbprintable))
|
||||||
|
return base64.RawURLEncoding.EncodeToString(sha[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParsePrivateKey will try to parse the bytes you give it
|
||||||
|
// in any of the supported formats: PEM, DER, PKCS8, PKCS1, SEC1, and JWK
|
||||||
|
func ParsePrivateKey(block []byte) (PrivateKey, error) {
|
||||||
|
blocks, err := getPEMBytes(block)
|
||||||
|
if nil != err {
|
||||||
|
return nil, ErrParsePrivateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse PEM blocks (openssl generates junk metadata blocks for ECs)
|
||||||
|
// or the original DER, or the JWK
|
||||||
|
for i := range blocks {
|
||||||
|
block = blocks[i]
|
||||||
|
if key, err := parsePrivateKey(block); nil == err {
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range blocks {
|
||||||
|
block = blocks[i]
|
||||||
|
if _, err := parsePublicKey(block); nil == err {
|
||||||
|
return nil, ErrUnexpectedPublicKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we didn't parse a key arleady, we failed
|
||||||
|
return nil, ErrParsePrivateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParsePrivateKeyString calls ParsePrivateKey([]byte(key)) for all you lazy folk.
|
||||||
|
func ParsePrivateKeyString(block string) (PrivateKey, error) {
|
||||||
|
return ParsePrivateKey([]byte(block))
|
||||||
|
}
|
||||||
|
|
||||||
|
func parsePrivateKey(der []byte) (PrivateKey, error) {
|
||||||
|
var key PrivateKey
|
||||||
|
|
||||||
|
//fmt.Println("1. ParsePKCS8PrivateKey")
|
||||||
|
xkey, err := x509.ParsePKCS8PrivateKey(der)
|
||||||
|
if nil == err {
|
||||||
|
switch k := xkey.(type) {
|
||||||
|
case *rsa.PrivateKey:
|
||||||
|
key = k
|
||||||
|
case *ecdsa.PrivateKey:
|
||||||
|
key = k
|
||||||
|
default:
|
||||||
|
err = errors.New("Only RSA and ECDSA (EC) Private Keys are supported")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if nil != err {
|
||||||
|
//fmt.Println("2. ParseECPrivateKey")
|
||||||
|
key, err = x509.ParseECPrivateKey(der)
|
||||||
|
if nil != err {
|
||||||
|
//fmt.Println("3. ParsePKCS1PrivateKey")
|
||||||
|
key, err = x509.ParsePKCS1PrivateKey(der)
|
||||||
|
if nil != err {
|
||||||
|
//fmt.Println("4. ParseJWKPrivateKey")
|
||||||
|
key, err = ParseJWKPrivateKey(der)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// But did you know?
|
||||||
|
// You must return nil explicitly for interfaces
|
||||||
|
// https://golang.org/doc/faq#nil_error
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPEMBytes(block []byte) ([][]byte, error) {
|
||||||
|
var pemblock *pem.Block
|
||||||
|
var blocks = make([][]byte, 0, 1)
|
||||||
|
|
||||||
|
// Parse the PEM, if it's a pem
|
||||||
|
for {
|
||||||
|
pemblock, block = pem.Decode(block)
|
||||||
|
if nil != pemblock {
|
||||||
|
// got one block, there may be more
|
||||||
|
blocks = append(blocks, pemblock.Bytes)
|
||||||
|
} else {
|
||||||
|
// the last block was not a PEM block
|
||||||
|
// therefore the next isn't either
|
||||||
|
if 0 != len(block) {
|
||||||
|
blocks = append(blocks, block)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(blocks) > 0 {
|
||||||
|
return blocks, nil
|
||||||
|
}
|
||||||
|
return nil, errors.New("no PEM blocks found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParsePublicKey will try to parse the bytes you give it
|
||||||
|
// in any of the supported formats: PEM, DER, PKIX/SPKI, PKCS1, x509 Certificate, and JWK
|
||||||
|
func ParsePublicKey(block []byte) (PublicKey, error) {
|
||||||
|
blocks, err := getPEMBytes(block)
|
||||||
|
if nil != err {
|
||||||
|
return nil, ErrParsePublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse PEM blocks (openssl generates junk metadata blocks for ECs)
|
||||||
|
// or the original DER, or the JWK
|
||||||
|
for i := range blocks {
|
||||||
|
block = blocks[i]
|
||||||
|
if key, err := parsePublicKey(block); nil == err {
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range blocks {
|
||||||
|
block = blocks[i]
|
||||||
|
if _, err := parsePrivateKey(block); nil == err {
|
||||||
|
return nil, ErrUnexpectedPrivateKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we didn't parse a key arleady, we failed
|
||||||
|
return nil, ErrParsePublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParsePublicKeyString calls ParsePublicKey([]byte(key)) for all you lazy folk.
|
||||||
|
func ParsePublicKeyString(block string) (PublicKey, error) {
|
||||||
|
return ParsePublicKey([]byte(block))
|
||||||
|
}
|
||||||
|
|
||||||
|
func parsePublicKey(der []byte) (PublicKey, error) {
|
||||||
|
cert, err := x509.ParseCertificate(der)
|
||||||
|
if nil == err {
|
||||||
|
switch k := cert.PublicKey.(type) {
|
||||||
|
case *rsa.PublicKey:
|
||||||
|
return NewPublicKey(k), nil
|
||||||
|
case *ecdsa.PublicKey:
|
||||||
|
return NewPublicKey(k), nil
|
||||||
|
default:
|
||||||
|
return nil, errors.New("Only RSA and ECDSA (EC) Public Keys are supported")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//fmt.Println("1. ParsePKIXPublicKey")
|
||||||
|
xkey, err := x509.ParsePKIXPublicKey(der)
|
||||||
|
if nil == err {
|
||||||
|
switch k := xkey.(type) {
|
||||||
|
case *rsa.PublicKey:
|
||||||
|
return NewPublicKey(k), nil
|
||||||
|
case *ecdsa.PublicKey:
|
||||||
|
return NewPublicKey(k), nil
|
||||||
|
default:
|
||||||
|
return nil, errors.New("Only RSA and ECDSA (EC) Public Keys are supported")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//fmt.Println("3. ParsePKCS1PrublicKey")
|
||||||
|
rkey, err := x509.ParsePKCS1PublicKey(der)
|
||||||
|
if nil == err {
|
||||||
|
//fmt.Println("4. ParseJWKPublicKey")
|
||||||
|
return NewPublicKey(rkey), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return ParseJWKPublicKey(der)
|
||||||
|
|
||||||
|
/*
|
||||||
|
// But did you know?
|
||||||
|
// You must return nil explicitly for interfaces
|
||||||
|
// https://golang.org/doc/faq#nil_error
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewJWKPublicKey contstructs a PublicKey from the relevant pieces a map[string]string (generic JSON)
|
||||||
|
func NewJWKPublicKey(m map[string]string) (PublicKey, error) {
|
||||||
|
switch m["kty"] {
|
||||||
|
case "RSA":
|
||||||
|
return parseRSAPublicKey(m)
|
||||||
|
case "EC":
|
||||||
|
return parseECPublicKey(m)
|
||||||
|
default:
|
||||||
|
return nil, ErrInvalidKeyType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseJWKPublicKey parses a JSON-encoded JWK and returns a PublicKey, or a (hopefully) helpful error message
|
||||||
|
func ParseJWKPublicKey(b []byte) (PublicKey, error) {
|
||||||
|
// RSA and EC have "d" as a private part
|
||||||
|
if bytes.Contains(b, []byte(`"d"`)) {
|
||||||
|
return nil, ErrUnexpectedPrivateKey
|
||||||
|
}
|
||||||
|
return newJWKPublicKey(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseJWKPublicKeyString calls ParseJWKPublicKey([]byte(key)) for all you lazy folk.
|
||||||
|
func ParseJWKPublicKeyString(s string) (PublicKey, error) {
|
||||||
|
if strings.Contains(s, `"d"`) {
|
||||||
|
return nil, ErrUnexpectedPrivateKey
|
||||||
|
}
|
||||||
|
return newJWKPublicKey(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeJWKPublicKey stream-decodes a JSON-encoded JWK and returns a PublicKey, or a (hopefully) helpful error message
|
||||||
|
func DecodeJWKPublicKey(r io.Reader) (PublicKey, error) {
|
||||||
|
m := make(map[string]string)
|
||||||
|
if err := json.NewDecoder(r).Decode(&m); nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if d := m["d"]; "" != d {
|
||||||
|
return nil, ErrUnexpectedPrivateKey
|
||||||
|
}
|
||||||
|
return newJWKPublicKey(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// the underpinnings of the parser as used by the typesafe wrappers
|
||||||
|
func newJWKPublicKey(data interface{}) (PublicKey, error) {
|
||||||
|
var m map[string]string
|
||||||
|
|
||||||
|
switch d := data.(type) {
|
||||||
|
case map[string]string:
|
||||||
|
m = d
|
||||||
|
case string:
|
||||||
|
if err := json.Unmarshal([]byte(d), &m); nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
case []byte:
|
||||||
|
if err := json.Unmarshal(d, &m); nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
panic("Developer Error: unsupported interface type")
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewJWKPublicKey(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseJWKPrivateKey parses a JSON-encoded JWK and returns a PrivateKey, or a (hopefully) helpful error message
|
||||||
|
func ParseJWKPrivateKey(b []byte) (PrivateKey, error) {
|
||||||
|
var m map[string]string
|
||||||
|
if err := json.Unmarshal(b, &m); nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch m["kty"] {
|
||||||
|
case "RSA":
|
||||||
|
return parseRSAPrivateKey(m)
|
||||||
|
case "EC":
|
||||||
|
return parseECPrivateKey(m)
|
||||||
|
default:
|
||||||
|
return nil, ErrInvalidKeyType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseRSAPublicKey(m map[string]string) (*RSAPublicKey, error) {
|
||||||
|
// TODO grab expiry?
|
||||||
|
kid, _ := m["kid"]
|
||||||
|
n, _ := base64.RawURLEncoding.DecodeString(m["n"])
|
||||||
|
e, _ := base64.RawURLEncoding.DecodeString(m["e"])
|
||||||
|
if 0 == len(n) || 0 == len(e) {
|
||||||
|
return nil, ErrParseJWK
|
||||||
|
}
|
||||||
|
ni := &big.Int{}
|
||||||
|
ni.SetBytes(n)
|
||||||
|
ei := &big.Int{}
|
||||||
|
ei.SetBytes(e)
|
||||||
|
|
||||||
|
pub := &rsa.PublicKey{
|
||||||
|
N: ni,
|
||||||
|
E: int(ei.Int64()),
|
||||||
|
}
|
||||||
|
|
||||||
|
return &RSAPublicKey{
|
||||||
|
PublicKey: pub,
|
||||||
|
KID: kid,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseRSAPrivateKey(m map[string]string) (key *rsa.PrivateKey, err error) {
|
||||||
|
pub, err := parseRSAPublicKey(m)
|
||||||
|
if nil != err {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
d, _ := base64.RawURLEncoding.DecodeString(m["d"])
|
||||||
|
p, _ := base64.RawURLEncoding.DecodeString(m["p"])
|
||||||
|
q, _ := base64.RawURLEncoding.DecodeString(m["q"])
|
||||||
|
dp, _ := base64.RawURLEncoding.DecodeString(m["dp"])
|
||||||
|
dq, _ := base64.RawURLEncoding.DecodeString(m["dq"])
|
||||||
|
qinv, _ := base64.RawURLEncoding.DecodeString(m["qi"])
|
||||||
|
if 0 == len(d) || 0 == len(p) || 0 == len(dp) || 0 == len(dq) || 0 == len(qinv) {
|
||||||
|
return nil, ErrParseJWK
|
||||||
|
}
|
||||||
|
|
||||||
|
di := &big.Int{}
|
||||||
|
di.SetBytes(d)
|
||||||
|
pi := &big.Int{}
|
||||||
|
pi.SetBytes(p)
|
||||||
|
qi := &big.Int{}
|
||||||
|
qi.SetBytes(q)
|
||||||
|
dpi := &big.Int{}
|
||||||
|
dpi.SetBytes(dp)
|
||||||
|
dqi := &big.Int{}
|
||||||
|
dqi.SetBytes(dq)
|
||||||
|
qinvi := &big.Int{}
|
||||||
|
qinvi.SetBytes(qinv)
|
||||||
|
|
||||||
|
key = &rsa.PrivateKey{
|
||||||
|
PublicKey: *pub.PublicKey,
|
||||||
|
D: di,
|
||||||
|
Primes: []*big.Int{pi, qi},
|
||||||
|
Precomputed: rsa.PrecomputedValues{
|
||||||
|
Dp: dpi,
|
||||||
|
Dq: dqi,
|
||||||
|
Qinv: qinvi,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseECPublicKey(m map[string]string) (*ECPublicKey, error) {
|
||||||
|
// TODO grab expiry?
|
||||||
|
kid, _ := m["kid"]
|
||||||
|
x, _ := base64.RawURLEncoding.DecodeString(m["x"])
|
||||||
|
y, _ := base64.RawURLEncoding.DecodeString(m["y"])
|
||||||
|
if 0 == len(x) || 0 == len(y) || 0 == len(m["crv"]) {
|
||||||
|
return nil, ErrParseJWK
|
||||||
|
}
|
||||||
|
|
||||||
|
xi := &big.Int{}
|
||||||
|
xi.SetBytes(x)
|
||||||
|
|
||||||
|
yi := &big.Int{}
|
||||||
|
yi.SetBytes(y)
|
||||||
|
|
||||||
|
var crv elliptic.Curve
|
||||||
|
switch m["crv"] {
|
||||||
|
case "P-256":
|
||||||
|
crv = elliptic.P256()
|
||||||
|
case "P-384":
|
||||||
|
crv = elliptic.P384()
|
||||||
|
case "P-521":
|
||||||
|
crv = elliptic.P521()
|
||||||
|
default:
|
||||||
|
return nil, ErrInvalidCurve
|
||||||
|
}
|
||||||
|
|
||||||
|
pub := &ecdsa.PublicKey{
|
||||||
|
Curve: crv,
|
||||||
|
X: xi,
|
||||||
|
Y: yi,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ECPublicKey{
|
||||||
|
PublicKey: pub,
|
||||||
|
KID: kid,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseECPrivateKey(m map[string]string) (*ecdsa.PrivateKey, error) {
|
||||||
|
pub, err := parseECPublicKey(m)
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
d, _ := base64.RawURLEncoding.DecodeString(m["d"])
|
||||||
|
if 0 == len(d) {
|
||||||
|
return nil, ErrParseJWK
|
||||||
|
}
|
||||||
|
di := &big.Int{}
|
||||||
|
di.SetBytes(d)
|
||||||
|
|
||||||
|
return &ecdsa.PrivateKey{
|
||||||
|
PublicKey: *pub.PublicKey,
|
||||||
|
D: di,
|
||||||
|
}, nil
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
.DS_Store
|
|
@ -0,0 +1,8 @@
|
||||||
|
language: go
|
||||||
|
|
||||||
|
go:
|
||||||
|
- 1.x
|
||||||
|
|
||||||
|
os:
|
||||||
|
- linux
|
||||||
|
- osx
|
|
@ -0,0 +1,23 @@
|
||||||
|
Copyright (c) 2013 John Barton
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of this software and associated documentation files (the
|
||||||
|
"Software"), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be
|
||||||
|
included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
|
@ -0,0 +1,163 @@
|
||||||
|
# GoDotEnv [![Build Status](https://travis-ci.org/joho/godotenv.svg?branch=master)](https://travis-ci.org/joho/godotenv) [![Build status](https://ci.appveyor.com/api/projects/status/9v40vnfvvgde64u4?svg=true)](https://ci.appveyor.com/project/joho/godotenv) [![Go Report Card](https://goreportcard.com/badge/github.com/joho/godotenv)](https://goreportcard.com/report/github.com/joho/godotenv)
|
||||||
|
|
||||||
|
A Go (golang) port of the Ruby dotenv project (which loads env vars from a .env file)
|
||||||
|
|
||||||
|
From the original Library:
|
||||||
|
|
||||||
|
> Storing configuration in the environment is one of the tenets of a twelve-factor app. Anything that is likely to change between deployment environments–such as resource handles for databases or credentials for external services–should be extracted from the code into environment variables.
|
||||||
|
>
|
||||||
|
> But it is not always practical to set environment variables on development machines or continuous integration servers where multiple projects are run. Dotenv load variables from a .env file into ENV when the environment is bootstrapped.
|
||||||
|
|
||||||
|
It can be used as a library (for loading in env for your own daemons etc) or as a bin command.
|
||||||
|
|
||||||
|
There is test coverage and CI for both linuxish and windows environments, but I make no guarantees about the bin version working on windows.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
As a library
|
||||||
|
|
||||||
|
```shell
|
||||||
|
go get github.com/joho/godotenv
|
||||||
|
```
|
||||||
|
|
||||||
|
or if you want to use it as a bin command
|
||||||
|
```shell
|
||||||
|
go get github.com/joho/godotenv/cmd/godotenv
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Add your application configuration to your `.env` file in the root of your project:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
S3_BUCKET=YOURS3BUCKET
|
||||||
|
SECRET_KEY=YOURSECRETKEYGOESHERE
|
||||||
|
```
|
||||||
|
|
||||||
|
Then in your Go app you can do something like
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
err := godotenv.Load()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Error loading .env file")
|
||||||
|
}
|
||||||
|
|
||||||
|
s3Bucket := os.Getenv("S3_BUCKET")
|
||||||
|
secretKey := os.Getenv("SECRET_KEY")
|
||||||
|
|
||||||
|
// now do something with s3 or whatever
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If you're even lazier than that, you can just take advantage of the autoload package which will read in `.env` on import
|
||||||
|
|
||||||
|
```go
|
||||||
|
import _ "github.com/joho/godotenv/autoload"
|
||||||
|
```
|
||||||
|
|
||||||
|
While `.env` in the project root is the default, you don't have to be constrained, both examples below are 100% legit
|
||||||
|
|
||||||
|
```go
|
||||||
|
_ = godotenv.Load("somerandomfile")
|
||||||
|
_ = godotenv.Load("filenumberone.env", "filenumbertwo.env")
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to be really fancy with your env file you can do comments and exports (below is a valid env file)
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# I am a comment and that is OK
|
||||||
|
SOME_VAR=someval
|
||||||
|
FOO=BAR # comments at line end are OK too
|
||||||
|
export BAR=BAZ
|
||||||
|
```
|
||||||
|
|
||||||
|
Or finally you can do YAML(ish) style
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
FOO: bar
|
||||||
|
BAR: baz
|
||||||
|
```
|
||||||
|
|
||||||
|
as a final aside, if you don't want godotenv munging your env you can just get a map back instead
|
||||||
|
|
||||||
|
```go
|
||||||
|
var myEnv map[string]string
|
||||||
|
myEnv, err := godotenv.Read()
|
||||||
|
|
||||||
|
s3Bucket := myEnv["S3_BUCKET"]
|
||||||
|
```
|
||||||
|
|
||||||
|
... or from an `io.Reader` instead of a local file
|
||||||
|
|
||||||
|
```go
|
||||||
|
reader := getRemoteFile()
|
||||||
|
myEnv, err := godotenv.Parse(reader)
|
||||||
|
```
|
||||||
|
|
||||||
|
... or from a `string` if you so desire
|
||||||
|
|
||||||
|
```go
|
||||||
|
content := getRemoteFileContent()
|
||||||
|
myEnv, err := godotenv.Unmarshal(content)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Command Mode
|
||||||
|
|
||||||
|
Assuming you've installed the command as above and you've got `$GOPATH/bin` in your `$PATH`
|
||||||
|
|
||||||
|
```
|
||||||
|
godotenv -f /some/path/to/.env some_command with some args
|
||||||
|
```
|
||||||
|
|
||||||
|
If you don't specify `-f` it will fall back on the default of loading `.env` in `PWD`
|
||||||
|
|
||||||
|
### Writing Env Files
|
||||||
|
|
||||||
|
Godotenv can also write a map representing the environment to a correctly-formatted and escaped file
|
||||||
|
|
||||||
|
```go
|
||||||
|
env, err := godotenv.Unmarshal("KEY=value")
|
||||||
|
err := godotenv.Write(env, "./.env")
|
||||||
|
```
|
||||||
|
|
||||||
|
... or to a string
|
||||||
|
|
||||||
|
```go
|
||||||
|
env, err := godotenv.Unmarshal("KEY=value")
|
||||||
|
content, err := godotenv.Marshal(env)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Contributions are most welcome! The parser itself is pretty stupidly naive and I wouldn't be surprised if it breaks with edge cases.
|
||||||
|
|
||||||
|
*code changes without tests will not be accepted*
|
||||||
|
|
||||||
|
1. Fork it
|
||||||
|
2. Create your feature branch (`git checkout -b my-new-feature`)
|
||||||
|
3. Commit your changes (`git commit -am 'Added some feature'`)
|
||||||
|
4. Push to the branch (`git push origin my-new-feature`)
|
||||||
|
5. Create new Pull Request
|
||||||
|
|
||||||
|
## Releases
|
||||||
|
|
||||||
|
Releases should follow [Semver](http://semver.org/) though the first couple of releases are `v1` and `v1.1`.
|
||||||
|
|
||||||
|
Use [annotated tags for all releases](https://github.com/joho/godotenv/issues/30). Example `git tag -a v1.2.1`
|
||||||
|
|
||||||
|
## CI
|
||||||
|
|
||||||
|
Linux: [![Build Status](https://travis-ci.org/joho/godotenv.svg?branch=master)](https://travis-ci.org/joho/godotenv) Windows: [![Build status](https://ci.appveyor.com/api/projects/status/9v40vnfvvgde64u4)](https://ci.appveyor.com/project/joho/godotenv)
|
||||||
|
|
||||||
|
## Who?
|
||||||
|
|
||||||
|
The original library [dotenv](https://github.com/bkeepers/dotenv) was written by [Brandon Keepers](http://opensoul.org/), and this port was done by [John Barton](https://johnbarton.co/) based off the tests/fixtures in the original library.
|
|
@ -0,0 +1,15 @@
|
||||||
|
package autoload
|
||||||
|
|
||||||
|
/*
|
||||||
|
You can just read the .env file on import just by doing
|
||||||
|
|
||||||
|
import _ "github.com/joho/godotenv/autoload"
|
||||||
|
|
||||||
|
And bob's your mother's brother
|
||||||
|
*/
|
||||||
|
|
||||||
|
import "github.com/joho/godotenv"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
godotenv.Load()
|
||||||
|
}
|
|
@ -0,0 +1,346 @@
|
||||||
|
// Package godotenv is a go port of the ruby dotenv library (https://github.com/bkeepers/dotenv)
|
||||||
|
//
|
||||||
|
// Examples/readme can be found on the github page at https://github.com/joho/godotenv
|
||||||
|
//
|
||||||
|
// The TL;DR is that you make a .env file that looks something like
|
||||||
|
//
|
||||||
|
// SOME_ENV_VAR=somevalue
|
||||||
|
//
|
||||||
|
// and then in your go code you can call
|
||||||
|
//
|
||||||
|
// godotenv.Load()
|
||||||
|
//
|
||||||
|
// and all the env vars declared in .env will be available through os.Getenv("SOME_ENV_VAR")
|
||||||
|
package godotenv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const doubleQuoteSpecialChars = "\\\n\r\"!$`"
|
||||||
|
|
||||||
|
// Load will read your env file(s) and load them into ENV for this process.
|
||||||
|
//
|
||||||
|
// Call this function as close as possible to the start of your program (ideally in main)
|
||||||
|
//
|
||||||
|
// If you call Load without any args it will default to loading .env in the current path
|
||||||
|
//
|
||||||
|
// You can otherwise tell it which files to load (there can be more than one) like
|
||||||
|
//
|
||||||
|
// godotenv.Load("fileone", "filetwo")
|
||||||
|
//
|
||||||
|
// It's important to note that it WILL NOT OVERRIDE an env variable that already exists - consider the .env file to set dev vars or sensible defaults
|
||||||
|
func Load(filenames ...string) (err error) {
|
||||||
|
filenames = filenamesOrDefault(filenames)
|
||||||
|
|
||||||
|
for _, filename := range filenames {
|
||||||
|
err = loadFile(filename, false)
|
||||||
|
if err != nil {
|
||||||
|
return // return early on a spazout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overload will read your env file(s) and load them into ENV for this process.
|
||||||
|
//
|
||||||
|
// Call this function as close as possible to the start of your program (ideally in main)
|
||||||
|
//
|
||||||
|
// If you call Overload without any args it will default to loading .env in the current path
|
||||||
|
//
|
||||||
|
// You can otherwise tell it which files to load (there can be more than one) like
|
||||||
|
//
|
||||||
|
// godotenv.Overload("fileone", "filetwo")
|
||||||
|
//
|
||||||
|
// It's important to note this WILL OVERRIDE an env variable that already exists - consider the .env file to forcefilly set all vars.
|
||||||
|
func Overload(filenames ...string) (err error) {
|
||||||
|
filenames = filenamesOrDefault(filenames)
|
||||||
|
|
||||||
|
for _, filename := range filenames {
|
||||||
|
err = loadFile(filename, true)
|
||||||
|
if err != nil {
|
||||||
|
return // return early on a spazout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read all env (with same file loading semantics as Load) but return values as
|
||||||
|
// a map rather than automatically writing values into env
|
||||||
|
func Read(filenames ...string) (envMap map[string]string, err error) {
|
||||||
|
filenames = filenamesOrDefault(filenames)
|
||||||
|
envMap = make(map[string]string)
|
||||||
|
|
||||||
|
for _, filename := range filenames {
|
||||||
|
individualEnvMap, individualErr := readFile(filename)
|
||||||
|
|
||||||
|
if individualErr != nil {
|
||||||
|
err = individualErr
|
||||||
|
return // return early on a spazout
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value := range individualEnvMap {
|
||||||
|
envMap[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse reads an env file from io.Reader, returning a map of keys and values.
|
||||||
|
func Parse(r io.Reader) (envMap map[string]string, err error) {
|
||||||
|
envMap = make(map[string]string)
|
||||||
|
|
||||||
|
var lines []string
|
||||||
|
scanner := bufio.NewScanner(r)
|
||||||
|
for scanner.Scan() {
|
||||||
|
lines = append(lines, scanner.Text())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = scanner.Err(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, fullLine := range lines {
|
||||||
|
if !isIgnoredLine(fullLine) {
|
||||||
|
var key, value string
|
||||||
|
key, value, err = parseLine(fullLine, envMap)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
envMap[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Unmarshal reads an env file from a string, returning a map of keys and values.
|
||||||
|
func Unmarshal(str string) (envMap map[string]string, err error) {
|
||||||
|
return Parse(strings.NewReader(str))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exec loads env vars from the specified filenames (empty map falls back to default)
|
||||||
|
// then executes the cmd specified.
|
||||||
|
//
|
||||||
|
// Simply hooks up os.Stdin/err/out to the command and calls Run()
|
||||||
|
//
|
||||||
|
// If you want more fine grained control over your command it's recommended
|
||||||
|
// that you use `Load()` or `Read()` and the `os/exec` package yourself.
|
||||||
|
func Exec(filenames []string, cmd string, cmdArgs []string) error {
|
||||||
|
Load(filenames...)
|
||||||
|
|
||||||
|
command := exec.Command(cmd, cmdArgs...)
|
||||||
|
command.Stdin = os.Stdin
|
||||||
|
command.Stdout = os.Stdout
|
||||||
|
command.Stderr = os.Stderr
|
||||||
|
return command.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write serializes the given environment and writes it to a file
|
||||||
|
func Write(envMap map[string]string, filename string) error {
|
||||||
|
content, error := Marshal(envMap)
|
||||||
|
if error != nil {
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
file, error := os.Create(filename)
|
||||||
|
if error != nil {
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
_, err := file.WriteString(content)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal outputs the given environment as a dotenv-formatted environment file.
|
||||||
|
// Each line is in the format: KEY="VALUE" where VALUE is backslash-escaped.
|
||||||
|
func Marshal(envMap map[string]string) (string, error) {
|
||||||
|
lines := make([]string, 0, len(envMap))
|
||||||
|
for k, v := range envMap {
|
||||||
|
lines = append(lines, fmt.Sprintf(`%s="%s"`, k, doubleQuoteEscape(v)))
|
||||||
|
}
|
||||||
|
sort.Strings(lines)
|
||||||
|
return strings.Join(lines, "\n"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func filenamesOrDefault(filenames []string) []string {
|
||||||
|
if len(filenames) == 0 {
|
||||||
|
return []string{".env"}
|
||||||
|
}
|
||||||
|
return filenames
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadFile(filename string, overload bool) error {
|
||||||
|
envMap, err := readFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
currentEnv := map[string]bool{}
|
||||||
|
rawEnv := os.Environ()
|
||||||
|
for _, rawEnvLine := range rawEnv {
|
||||||
|
key := strings.Split(rawEnvLine, "=")[0]
|
||||||
|
currentEnv[key] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value := range envMap {
|
||||||
|
if !currentEnv[key] || overload {
|
||||||
|
os.Setenv(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readFile(filename string) (envMap map[string]string, err error) {
|
||||||
|
file, err := os.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
return Parse(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseLine(line string, envMap map[string]string) (key string, value string, err error) {
|
||||||
|
if len(line) == 0 {
|
||||||
|
err = errors.New("zero length string")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ditch the comments (but keep quoted hashes)
|
||||||
|
if strings.Contains(line, "#") {
|
||||||
|
segmentsBetweenHashes := strings.Split(line, "#")
|
||||||
|
quotesAreOpen := false
|
||||||
|
var segmentsToKeep []string
|
||||||
|
for _, segment := range segmentsBetweenHashes {
|
||||||
|
if strings.Count(segment, "\"") == 1 || strings.Count(segment, "'") == 1 {
|
||||||
|
if quotesAreOpen {
|
||||||
|
quotesAreOpen = false
|
||||||
|
segmentsToKeep = append(segmentsToKeep, segment)
|
||||||
|
} else {
|
||||||
|
quotesAreOpen = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(segmentsToKeep) == 0 || quotesAreOpen {
|
||||||
|
segmentsToKeep = append(segmentsToKeep, segment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
line = strings.Join(segmentsToKeep, "#")
|
||||||
|
}
|
||||||
|
|
||||||
|
firstEquals := strings.Index(line, "=")
|
||||||
|
firstColon := strings.Index(line, ":")
|
||||||
|
splitString := strings.SplitN(line, "=", 2)
|
||||||
|
if firstColon != -1 && (firstColon < firstEquals || firstEquals == -1) {
|
||||||
|
//this is a yaml-style line
|
||||||
|
splitString = strings.SplitN(line, ":", 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(splitString) != 2 {
|
||||||
|
err = errors.New("Can't separate key from value")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the key
|
||||||
|
key = splitString[0]
|
||||||
|
if strings.HasPrefix(key, "export") {
|
||||||
|
key = strings.TrimPrefix(key, "export")
|
||||||
|
}
|
||||||
|
key = strings.Trim(key, " ")
|
||||||
|
|
||||||
|
// Parse the value
|
||||||
|
value = parseValue(splitString[1], envMap)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseValue(value string, envMap map[string]string) string {
|
||||||
|
|
||||||
|
// trim
|
||||||
|
value = strings.Trim(value, " ")
|
||||||
|
|
||||||
|
// check if we've got quoted values or possible escapes
|
||||||
|
if len(value) > 1 {
|
||||||
|
rs := regexp.MustCompile(`\A'(.*)'\z`)
|
||||||
|
singleQuotes := rs.FindStringSubmatch(value)
|
||||||
|
|
||||||
|
rd := regexp.MustCompile(`\A"(.*)"\z`)
|
||||||
|
doubleQuotes := rd.FindStringSubmatch(value)
|
||||||
|
|
||||||
|
if singleQuotes != nil || doubleQuotes != nil {
|
||||||
|
// pull the quotes off the edges
|
||||||
|
value = value[1 : len(value)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if doubleQuotes != nil {
|
||||||
|
// expand newlines
|
||||||
|
escapeRegex := regexp.MustCompile(`\\.`)
|
||||||
|
value = escapeRegex.ReplaceAllStringFunc(value, func(match string) string {
|
||||||
|
c := strings.TrimPrefix(match, `\`)
|
||||||
|
switch c {
|
||||||
|
case "n":
|
||||||
|
return "\n"
|
||||||
|
case "r":
|
||||||
|
return "\r"
|
||||||
|
default:
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// unescape characters
|
||||||
|
e := regexp.MustCompile(`\\([^$])`)
|
||||||
|
value = e.ReplaceAllString(value, "$1")
|
||||||
|
}
|
||||||
|
|
||||||
|
if singleQuotes == nil {
|
||||||
|
value = expandVariables(value, envMap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func expandVariables(v string, m map[string]string) string {
|
||||||
|
r := regexp.MustCompile(`(\\)?(\$)(\()?\{?([A-Z0-9_]+)?\}?`)
|
||||||
|
|
||||||
|
return r.ReplaceAllStringFunc(v, func(s string) string {
|
||||||
|
submatch := r.FindStringSubmatch(s)
|
||||||
|
|
||||||
|
if submatch == nil {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
if submatch[1] == "\\" || submatch[2] == "(" {
|
||||||
|
return submatch[0][1:]
|
||||||
|
} else if submatch[4] != "" {
|
||||||
|
return m[submatch[4]]
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func isIgnoredLine(line string) bool {
|
||||||
|
trimmedLine := strings.Trim(line, " \n\t")
|
||||||
|
return len(trimmedLine) == 0 || strings.HasPrefix(trimmedLine, "#")
|
||||||
|
}
|
||||||
|
|
||||||
|
func doubleQuoteEscape(line string) string {
|
||||||
|
for _, c := range doubleQuoteSpecialChars {
|
||||||
|
toReplace := "\\" + string(c)
|
||||||
|
if c == '\n' {
|
||||||
|
toReplace = `\n`
|
||||||
|
}
|
||||||
|
if c == '\r' {
|
||||||
|
toReplace = `\r`
|
||||||
|
}
|
||||||
|
line = strings.Replace(line, string(c), toReplace, -1)
|
||||||
|
}
|
||||||
|
return line
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
# git.rootprojects.org/root/keypairs v0.5.2
|
||||||
|
git.rootprojects.org/root/keypairs
|
||||||
|
git.rootprojects.org/root/keypairs/keyfetch
|
||||||
|
git.rootprojects.org/root/keypairs/keyfetch/uncached
|
||||||
|
# github.com/joho/godotenv v1.3.0
|
||||||
|
github.com/joho/godotenv
|
||||||
|
github.com/joho/godotenv/autoload
|
Loading…
Reference in New Issue