Compare commits
	
		
			No commits in common. "285dc81dd767d8d1e5b021ec3bcf7e6094db8ccb" and "66e0639f48bb0e061e78a2830e0a98ab615fee75" have entirely different histories.
		
	
	
		
			285dc81dd7
			...
			66e0639f48
		
	
		
@ -1,5 +1,4 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "kty": "EC",
 | 
					 | 
				
			||||||
  "crv": "P-256",
 | 
					  "crv": "P-256",
 | 
				
			||||||
  "d": "GYAwlBHc2mPsj1lp315HbYOmKNJ7esmO3JAkZVn9nJs",
 | 
					  "d": "GYAwlBHc2mPsj1lp315HbYOmKNJ7esmO3JAkZVn9nJs",
 | 
				
			||||||
  "x": "ToL2HppsTESXQKvp7ED6NMgV4YnwbMeONexNry3KDNQ",
 | 
					  "x": "ToL2HppsTESXQKvp7ED6NMgV4YnwbMeONexNry3KDNQ",
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										28
									
								
								mockid.go
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								mockid.go
									
									
									
									
									
								
							@ -1,6 +1,7 @@
 | 
				
			|||||||
package main
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"encoding/json"
 | 
				
			||||||
	"flag"
 | 
						"flag"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"io/ioutil"
 | 
						"io/ioutil"
 | 
				
			||||||
@ -11,7 +12,6 @@ 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,13 +44,24 @@ func main() {
 | 
				
			|||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	privkey, err := keypairs.ParseJWKPrivateKey(jwkb)
 | 
						jwkm := map[string]string{}
 | 
				
			||||||
 | 
						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 {
 | 
				
			||||||
@ -69,7 +80,7 @@ func main() {
 | 
				
			|||||||
		os.Exit(1)
 | 
							os.Exit(1)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	mockid.Route(jwksPrefix, privkey)
 | 
						mockid.Route(jwksPrefix, priv, jwk)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	fs := http.FileServer(http.Dir("./public"))
 | 
						fs := http.FileServer(http.Dir("./public"))
 | 
				
			||||||
	http.Handle("/", fs)
 | 
						http.Handle("/", fs)
 | 
				
			||||||
@ -86,12 +97,11 @@ func main() {
 | 
				
			|||||||
		done <- true
 | 
							done <- true
 | 
				
			||||||
	}()
 | 
						}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// TODO privB := keypairs.MarshalJWKPrivateKey(privkey)
 | 
						b, _ := json.Marshal(jwk)
 | 
				
			||||||
	privB := mockid.MarshalJWKPrivateKey(privkey)
 | 
						fmt.Printf("Private Key:\n\t%s\n", string(b))
 | 
				
			||||||
	fmt.Printf("Private Key:\n\t%s\n", string(privB))
 | 
						b, _ = json.Marshal(jwk.PublicJWK)
 | 
				
			||||||
	pubB := keypairs.MarshalJWKPublicKey(keypairs.NewPublicKey(privkey.Public()))
 | 
						fmt.Printf("Public Key:\n\t%s\n", string(b))
 | 
				
			||||||
	fmt.Printf("Public Key:\n\t%s\n", string(pubB))
 | 
						protected, payload, token := mockid.GenToken(host, priv, url.Values{})
 | 
				
			||||||
	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)
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										183
									
								
								mockid/mockid.go
									
									
									
									
									
								
							
							
						
						
									
										183
									
								
								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,9 +21,13 @@ 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"`
 | 
				
			||||||
@ -32,34 +36,15 @@ 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, privkey keypairs.PrivateKey) {
 | 
					func Route(jwksPrefix string, priv *ecdsa.PrivateKey, jwk *PrivateJWK) {
 | 
				
			||||||
	pubkey := keypairs.NewPublicKey(privkey.Public())
 | 
						pub := &priv.PublicKey
 | 
				
			||||||
 | 
						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)
 | 
				
			||||||
@ -125,7 +110,7 @@ func Route(jwksPrefix string, privkey keypairs.PrivateKey) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	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), privkey, r.URL.Query())
 | 
							_, _, token := GenToken(getBaseURL(r), priv, r.URL.Query())
 | 
				
			||||||
		fmt.Fprintf(w, token)
 | 
							fmt.Fprintf(w, token)
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -150,7 +135,7 @@ func Route(jwksPrefix string, privkey keypairs.PrivateKey) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		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>.<payload>.<signature>", http.StatusBadRequest)
 | 
								http.Error(w, "Bad Format: token should be in the format of <protected-header>.<body>.<signature>", http.StatusBadRequest)
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		protected64 := parts[0]
 | 
							protected64 := parts[0]
 | 
				
			||||||
@ -164,7 +149,7 @@ func Route(jwksPrefix string, privkey keypairs.PrivateKey) {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
		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 payload should be URL-safe base64 encoded", http.StatusBadRequest)
 | 
								http.Error(w, "Bad Format: token's body should be URL-safe base64 encoded", http.StatusBadRequest)
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		// TODO verify signature
 | 
							// TODO verify signature
 | 
				
			||||||
@ -192,12 +177,12 @@ func Route(jwksPrefix string, privkey keypairs.PrivateKey) {
 | 
				
			|||||||
		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 payload should be URL-safe base64-encoded JSON", http.StatusBadRequest)
 | 
								http.Error(w, "Bad Format: token's body 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, "payload.iss must exist to complement header.kid")
 | 
								errors = append(errors, "body.iss must exist to complement header.kid")
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		pub, err := keyfetch.OIDCJWK(kid, iss)
 | 
							pub, err := keyfetch.OIDCJWK(kid, iss)
 | 
				
			||||||
@ -208,16 +193,23 @@ func Route(jwksPrefix string, privkey keypairs.PrivateKey) {
 | 
				
			|||||||
		fmt.Println("fetched pub key:")
 | 
							fmt.Println("fetched pub key:")
 | 
				
			||||||
		fmt.Println(pub)
 | 
							fmt.Println(pub)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		inspected := &InspectableToken{
 | 
							inspected := struct {
 | 
				
			||||||
 | 
								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,
 | 
				
			||||||
			Payload:   data,
 | 
								Body:      data,
 | 
				
			||||||
			Signature: signature64,
 | 
								Signature: signature64,
 | 
				
			||||||
			Verified:  false,
 | 
								Verified:  false,
 | 
				
			||||||
			Errors:    errors,
 | 
								Errors:    errors,
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		tokenB, err := json.MarshalIndent(inspected, "", "    ")
 | 
							tokenB, err := json.Marshal(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)
 | 
				
			||||||
@ -247,19 +239,13 @@ func Route(jwksPrefix string, privkey keypairs.PrivateKey) {
 | 
				
			|||||||
			prefix = prefixes[0]
 | 
								prefix = prefixes[0]
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		_, _, token := GenToken(getBaseURL(r), privkey, r.URL.Query())
 | 
							_, _, token := GenToken(getBaseURL(r), priv, 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)
 | 
				
			||||||
		jwk := string(MarshalJWKPrivateKey(privkey))
 | 
							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 = 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) {
 | 
				
			||||||
@ -276,14 +262,10 @@ func Route(jwksPrefix string, privkey keypairs.PrivateKey) {
 | 
				
			|||||||
		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)
 | 
				
			||||||
			exp := strconv.FormatInt(time.Now().Add(15*time.Minute).Unix(), 10)
 | 
								jwkstr := fmt.Sprintf(
 | 
				
			||||||
			jwk := string(keypairs.MarshalJWKPublicKey(pubkey))
 | 
									`{ "keys": [ { "kty": "EC" , "crv": %q , "x": %q , "y": %q , "kid": %q , "ext": true , "key_ops": ["verify"] , "exp": %s } ] }`,
 | 
				
			||||||
			jwk = strings.Replace(jwk, `{"`, `{ "`, 1)
 | 
									jwk.Crv, jwk.X, jwk.Y, thumbprint, strconv.FormatInt(time.Now().Add(15*time.Minute).Unix(), 10),
 | 
				
			||||||
			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
 | 
				
			||||||
@ -480,15 +462,9 @@ func postRSA(jwksPrefix string, tok map[string]interface{}, w http.ResponseWrite
 | 
				
			|||||||
	)))
 | 
						)))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func GenToken(host string, privkey keypairs.PrivateKey, query url.Values) (string, string, string) {
 | 
					func GenToken(host string, priv *ecdsa.PrivateKey, query url.Values) (string, string, string) {
 | 
				
			||||||
	thumbprint := keypairs.ThumbprintPublicKey(keypairs.NewPublicKey(privkey.Public()))
 | 
						thumbprint := thumbprintKey(&priv.PublicKey)
 | 
				
			||||||
	// TODO keypairs.Alg(key)
 | 
						protected := fmt.Sprintf(`{"typ":"JWT","alg":"ES256","kid":"%s"}`, thumbprint)
 | 
				
			||||||
	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"))
 | 
				
			||||||
@ -505,22 +481,7 @@ func GenToken(host string, privkey keypairs.PrivateKey, query url.Values) (strin
 | 
				
			|||||||
	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)))
 | 
				
			||||||
	sig := JOSESign(privkey, hash[:])
 | 
						r, s, _ := ecdsa.Sign(rand.Reader, priv, hash[:])
 | 
				
			||||||
	sig64 := base64.RawURLEncoding.EncodeToString(sig)
 | 
					 | 
				
			||||||
	token := fmt.Sprintf("%s.%s.%s\n", protected64, payload64, sig64)
 | 
					 | 
				
			||||||
	return protected, payload, token
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// TODO: move to keypairs
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func JOSESign(privkey keypairs.PrivateKey, hash []byte) []byte {
 | 
					 | 
				
			||||||
	var sig []byte
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	switch k := privkey.(type) {
 | 
					 | 
				
			||||||
	case *rsa.PrivateKey:
 | 
					 | 
				
			||||||
		panic("TODO: implement rsa sign")
 | 
					 | 
				
			||||||
	case *ecdsa.PrivateKey:
 | 
					 | 
				
			||||||
		r, s, _ := ecdsa.Sign(rand.Reader, k, hash[:])
 | 
					 | 
				
			||||||
	rb := r.Bytes()
 | 
						rb := r.Bytes()
 | 
				
			||||||
	for len(rb) < 32 {
 | 
						for len(rb) < 32 {
 | 
				
			||||||
		rb = append([]byte{0}, rb...)
 | 
							rb = append([]byte{0}, rb...)
 | 
				
			||||||
@ -529,9 +490,38 @@ func JOSESign(privkey keypairs.PrivateKey, hash []byte) []byte {
 | 
				
			|||||||
	for len(rb) < 32 {
 | 
						for len(rb) < 32 {
 | 
				
			||||||
		sb = append([]byte{0}, sb...)
 | 
							sb = append([]byte{0}, sb...)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
		sig = append(rb, sb...)
 | 
						sig64 := base64.RawURLEncoding.EncodeToString(append(rb, sb...))
 | 
				
			||||||
 | 
						token := fmt.Sprintf("%s.%s.%s\n", protected64, payload64, sig64)
 | 
				
			||||||
 | 
						return protected, payload, token
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func ParseKey(jwk *PrivateJWK) *ecdsa.PrivateKey {
 | 
				
			||||||
 | 
						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,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return sig
 | 
					
 | 
				
			||||||
 | 
						db, _ := base64.RawURLEncoding.DecodeString(jwk.D)
 | 
				
			||||||
 | 
						di := &big.Int{}
 | 
				
			||||||
 | 
						di.SetBytes(db)
 | 
				
			||||||
 | 
						priv := &ecdsa.PrivateKey{
 | 
				
			||||||
 | 
							PublicKey: *pub,
 | 
				
			||||||
 | 
							D:         di,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return priv
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func thumbprintKey(pub *ecdsa.PublicKey) string {
 | 
				
			||||||
 | 
						minpub := []byte(fmt.Sprintf(`{"crv":%q,"kty":"EC","x":%q,"y":%q}`, "P-256", pub.X, pub.Y))
 | 
				
			||||||
 | 
						sha := sha256.Sum256(minpub)
 | 
				
			||||||
 | 
						return base64.RawURLEncoding.EncodeToString(sha[:])
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func issueNonce(w http.ResponseWriter, r *http.Request) {
 | 
					func issueNonce(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
@ -577,46 +567,3 @@ 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,
 | 
					 | 
				
			||||||
	))
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										21
									
								
								vendor/git.rootprojects.org/root/keypairs/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										21
									
								
								vendor/git.rootprojects.org/root/keypairs/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
								
							@ -1,21 +0,0 @@
 | 
				
			|||||||
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.
 | 
					 | 
				
			||||||
							
								
								
									
										63
									
								
								vendor/git.rootprojects.org/root/keypairs/README.md
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										63
									
								
								vendor/git.rootprojects.org/root/keypairs/README.md
									
									
									
										generated
									
									
										vendored
									
									
								
							@ -1,63 +0,0 @@
 | 
				
			|||||||
# 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>.
 | 
					 | 
				
			||||||
							
								
								
									
										40
									
								
								vendor/git.rootprojects.org/root/keypairs/doc.go
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										40
									
								
								vendor/git.rootprojects.org/root/keypairs/doc.go
									
									
									
										generated
									
									
										vendored
									
									
								
							@ -1,40 +0,0 @@
 | 
				
			|||||||
/*
 | 
					 | 
				
			||||||
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
 | 
					 | 
				
			||||||
							
								
								
									
										3
									
								
								vendor/git.rootprojects.org/root/keypairs/go.mod
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								vendor/git.rootprojects.org/root/keypairs/go.mod
									
									
									
										generated
									
									
										vendored
									
									
								
							@ -1,3 +0,0 @@
 | 
				
			|||||||
module git.rootprojects.org/root/keypairs
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
go 1.12
 | 
					 | 
				
			||||||
							
								
								
									
										516
									
								
								vendor/git.rootprojects.org/root/keypairs/keyfetch/fetch.go
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										516
									
								
								vendor/git.rootprojects.org/root/keypairs/keyfetch/fetch.go
									
									
									
										generated
									
									
										vendored
									
									
								
							@ -1,516 +0,0 @@
 | 
				
			|||||||
// 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
									
									
								
							
							
						
						
									
										183
									
								
								vendor/git.rootprojects.org/root/keypairs/keyfetch/uncached/fetch.go
									
									
									
										generated
									
									
										vendored
									
									
								
							@ -1,183 +0,0 @@
 | 
				
			|||||||
// 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, "/") + "/"
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										645
									
								
								vendor/git.rootprojects.org/root/keypairs/keypairs.go
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										645
									
								
								vendor/git.rootprojects.org/root/keypairs/keypairs.go
									
									
									
										generated
									
									
										vendored
									
									
								
							@ -1,645 +0,0 @@
 | 
				
			|||||||
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
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										1
									
								
								vendor/github.com/joho/godotenv/.gitignore
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								vendor/github.com/joho/godotenv/.gitignore
									
									
									
										generated
									
									
										vendored
									
									
								
							@ -1 +0,0 @@
 | 
				
			|||||||
.DS_Store
 | 
					 | 
				
			||||||
							
								
								
									
										8
									
								
								vendor/github.com/joho/godotenv/.travis.yml
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								vendor/github.com/joho/godotenv/.travis.yml
									
									
									
										generated
									
									
										vendored
									
									
								
							@ -1,8 +0,0 @@
 | 
				
			|||||||
language: go
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
go:
 | 
					 | 
				
			||||||
  - 1.x
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
os:
 | 
					 | 
				
			||||||
  - linux
 | 
					 | 
				
			||||||
  - osx
 | 
					 | 
				
			||||||
							
								
								
									
										23
									
								
								vendor/github.com/joho/godotenv/LICENCE
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										23
									
								
								vendor/github.com/joho/godotenv/LICENCE
									
									
									
										generated
									
									
										vendored
									
									
								
							@ -1,23 +0,0 @@
 | 
				
			|||||||
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.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
							
								
								
									
										163
									
								
								vendor/github.com/joho/godotenv/README.md
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										163
									
								
								vendor/github.com/joho/godotenv/README.md
									
									
									
										generated
									
									
										vendored
									
									
								
							@ -1,163 +0,0 @@
 | 
				
			|||||||
# GoDotEnv [](https://travis-ci.org/joho/godotenv) [](https://ci.appveyor.com/project/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: [](https://travis-ci.org/joho/godotenv) Windows: [](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.
 | 
					 | 
				
			||||||
							
								
								
									
										15
									
								
								vendor/github.com/joho/godotenv/autoload/autoload.go
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										15
									
								
								vendor/github.com/joho/godotenv/autoload/autoload.go
									
									
									
										generated
									
									
										vendored
									
									
								
							@ -1,15 +0,0 @@
 | 
				
			|||||||
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()
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										346
									
								
								vendor/github.com/joho/godotenv/godotenv.go
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										346
									
								
								vendor/github.com/joho/godotenv/godotenv.go
									
									
									
										generated
									
									
										vendored
									
									
								
							@ -1,346 +0,0 @@
 | 
				
			|||||||
// 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
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										7
									
								
								vendor/modules.txt
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								vendor/modules.txt
									
									
									
									
										vendored
									
									
								
							@ -1,7 +0,0 @@
 | 
				
			|||||||
# 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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user