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