diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e6441a5 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module git.rootprojects.org/root/libauth + +go 1.18 + +require git.rootprojects.org/root/keypairs v0.6.5 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..2991505 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +git.rootprojects.org/root/keypairs v0.6.5 h1:sdRAQD/O/JBS8+ZxUewXnY+cjQVDNH3TmcS+KtANZqA= +git.rootprojects.org/root/keypairs v0.6.5/go.mod h1:WGI8PadOp+4LjUuI+wNlSwcJwFtY8L9XuNjuO3213HA= diff --git a/vendor/git.rootprojects.org/root/keypairs/.gitignore b/vendor/git.rootprojects.org/root/keypairs/.gitignore new file mode 100644 index 0000000..9140b88 --- /dev/null +++ b/vendor/git.rootprojects.org/root/keypairs/.gitignore @@ -0,0 +1,5 @@ +/keypairs +/dist/ + +.DS_Store +.*.sw* diff --git a/vendor/git.rootprojects.org/root/keypairs/.goreleaser.yml b/vendor/git.rootprojects.org/root/keypairs/.goreleaser.yml new file mode 100644 index 0000000..9b6df83 --- /dev/null +++ b/vendor/git.rootprojects.org/root/keypairs/.goreleaser.yml @@ -0,0 +1,41 @@ +# This is an example goreleaser.yaml file with some sane defaults. +# Make sure to check the documentation at http://goreleaser.com +before: + hooks: + - go generate ./... +builds: + - id: keypairs + main: ./cmd/keypairs/keypairs.go + env: + - CGO_ENABLED=0 + flags: + - -mod=vendor + goos: + - linux + - windows + - darwin + - freebsd + goarch: + - amd64 + - arm + - arm64 +archives: + - replacements: + 386: i386 + amd64: x86-64 + arm64: aarch64 + format_overrides: + - goos: windows + format: zip +env_files: + github_token: ~/.config/goreleaser/github_token.txt +checksum: + name_template: 'checksums.txt' +snapshot: + name_template: "{{ .Tag }}-next" +changelog: + sort: asc + filters: + exclude: + - '^docs:' + - '^test:' diff --git a/vendor/git.rootprojects.org/root/keypairs/AUTHORS b/vendor/git.rootprojects.org/root/keypairs/AUTHORS new file mode 100644 index 0000000..12d2230 --- /dev/null +++ b/vendor/git.rootprojects.org/root/keypairs/AUTHORS @@ -0,0 +1 @@ +AJ ONeal (https://therootcompany.com) diff --git a/vendor/git.rootprojects.org/root/keypairs/LICENSE b/vendor/git.rootprojects.org/root/keypairs/LICENSE new file mode 100644 index 0000000..ca2baf4 --- /dev/null +++ b/vendor/git.rootprojects.org/root/keypairs/LICENSE @@ -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. diff --git a/vendor/git.rootprojects.org/root/keypairs/README.md b/vendor/git.rootprojects.org/root/keypairs/README.md new file mode 100644 index 0000000..f020b23 --- /dev/null +++ b/vendor/git.rootprojects.org/root/keypairs/README.md @@ -0,0 +1,63 @@ +# [keypairs](https://git.rootprojects.org/root/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) +``` + +# GoDoc API Documentation + +See + +# 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 . diff --git a/vendor/git.rootprojects.org/root/keypairs/cli_test.sh b/vendor/git.rootprojects.org/root/keypairs/cli_test.sh new file mode 100644 index 0000000..6420e26 --- /dev/null +++ b/vendor/git.rootprojects.org/root/keypairs/cli_test.sh @@ -0,0 +1,19 @@ +#!/bin/bash +set -u + +go build -mod=vendor cmd/keypairs/*.go +./keypairs gen > testkey.jwk.json 2> testpub.jwk.json + +./keypairs sign --exp 1h ./testkey.jwk.json '{"foo":"bar"}' > testjwt.txt 2> testjws.json + +echo "" +echo "Should pass:" +./keypairs verify ./testpub.jwk.json testjwt.txt > /dev/null +./keypairs verify ./testpub.jwk.json "$(cat testjwt.txt)" > /dev/null +./keypairs verify ./testpub.jwk.json testjws.json > /dev/null +./keypairs verify ./testpub.jwk.json "$(cat testjws.json)" > /dev/null + +echo "" +echo "Should fail:" +./keypairs sign --exp -1m ./testkey.jwk.json '{"bar":"foo"}' > errjwt.txt 2> errjws.json +./keypairs verify ./testpub.jwk.json errjwt.txt > /dev/null diff --git a/vendor/git.rootprojects.org/root/keypairs/doc.go b/vendor/git.rootprojects.org/root/keypairs/doc.go new file mode 100644 index 0000000..378c30c --- /dev/null +++ b/vendor/git.rootprojects.org/root/keypairs/doc.go @@ -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 diff --git a/vendor/git.rootprojects.org/root/keypairs/generate.go b/vendor/git.rootprojects.org/root/keypairs/generate.go new file mode 100644 index 0000000..13f99ec --- /dev/null +++ b/vendor/git.rootprojects.org/root/keypairs/generate.go @@ -0,0 +1,69 @@ +package keypairs + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "io" + mathrand "math/rand" + "time" +) + +var randReader io.Reader = rand.Reader +var allowMocking = false + +// KeyOptions are the things that we may need to know about a request to fulfill it properly +type keyOptions struct { + //Key string `json:"key"` + KeyType string `json:"kty"` + mockSeed int64 //`json:"-"` + //SeedStr string `json:"seed"` + //Claims Object `json:"claims"` + //Header Object `json:"header"` +} + +func (o *keyOptions) nextReader() io.Reader { + if allowMocking { + return o.maybeMockReader() + } + return randReader +} + +// NewDefaultPrivateKey generates a key with reasonable strength. +// Today that means a 256-bit equivalent - either RSA 2048 or EC P-256. +func NewDefaultPrivateKey() PrivateKey { + // insecure random is okay here, + // it's just used for a coin toss + mathrand.Seed(time.Now().UnixNano()) + coin := mathrand.Int() + + // the idea here is that we want to make + // it dead simple to support RSA and EC + // so it shouldn't matter which is used + if 0 == coin%2 { + return newPrivateKey(&keyOptions{ + KeyType: "RSA", + }) + } + return newPrivateKey(&keyOptions{ + KeyType: "EC", + }) +} + +// newPrivateKey generates a 256-bit entropy RSA or ECDSA private key +func newPrivateKey(opts *keyOptions) PrivateKey { + var privkey PrivateKey + + if "RSA" == opts.KeyType { + keylen := 2048 + privkey, _ = rsa.GenerateKey(opts.nextReader(), keylen) + if allowMocking { + privkey = maybeDerandomizeMockKey(privkey, keylen, opts) + } + } else { + // TODO: EC keys may also suffer the same random problems in the future + privkey, _ = ecdsa.GenerateKey(elliptic.P256(), opts.nextReader()) + } + return privkey +} diff --git a/vendor/git.rootprojects.org/root/keypairs/jwk.go b/vendor/git.rootprojects.org/root/keypairs/jwk.go new file mode 100644 index 0000000..2149fa6 --- /dev/null +++ b/vendor/git.rootprojects.org/root/keypairs/jwk.go @@ -0,0 +1,69 @@ +package keypairs + +import ( + "fmt" +) + +// JWK abstracts EC and RSA keys +type JWK interface { + marshalJWK() ([]byte, error) +} + +// ECJWK is the EC variant +type ECJWK struct { + KeyID string `json:"kid,omitempty"` + Curve string `json:"crv"` + X string `json:"x"` + Y string `json:"y"` + Use []string `json:"use,omitempty"` + Seed string `json:"_seed,omitempty"` +} + +func (k *ECJWK) marshalJWK() ([]byte, error) { + return []byte(fmt.Sprintf(`{"crv":%q,"kty":"EC","x":%q,"y":%q}`, k.Curve, k.X, k.Y)), nil +} + +// RSAJWK is the RSA variant +type RSAJWK struct { + KeyID string `json:"kid,omitempty"` + Exp string `json:"e"` + N string `json:"n"` + Use []string `json:"use,omitempty"` + Seed string `json:"_seed,omitempty"` +} + +func (k *RSAJWK) marshalJWK() ([]byte, error) { + return []byte(fmt.Sprintf(`{"e":%q,"kty":"RSA","n":%q}`, k.Exp, k.N)), nil +} + +/* +// ToPublicJWK exposes only the public parts +func ToPublicJWK(pubkey PublicKey) JWK { + switch k := pubkey.Key().(type) { + case *ecdsa.PublicKey: + return ECToPublicJWK(k) + case *rsa.PublicKey: + return RSAToPublicJWK(k) + default: + panic(errors.New("impossible key type")) + //return nil + } +} + +// ECToPublicJWK will output the most minimal version of an EC JWK (no key id, no "use" flag, nada) +func ECToPublicJWK(k *ecdsa.PublicKey) *ECJWK { + return &ECJWK{ + Curve: k.Curve.Params().Name, + X: base64.RawURLEncoding.EncodeToString(k.X.Bytes()), + Y: base64.RawURLEncoding.EncodeToString(k.Y.Bytes()), + } +} + +// RSAToPublicJWK will output the most minimal version of an RSA JWK (no key id, no "use" flag, nada) +func RSAToPublicJWK(p *rsa.PublicKey) *RSAJWK { + return &RSAJWK{ + Exp: base64.RawURLEncoding.EncodeToString(big.NewInt(int64(p.E)).Bytes()), + N: base64.RawURLEncoding.EncodeToString(p.N.Bytes()), + } +} +*/ diff --git a/vendor/git.rootprojects.org/root/keypairs/jws.go b/vendor/git.rootprojects.org/root/keypairs/jws.go new file mode 100644 index 0000000..9d27c39 --- /dev/null +++ b/vendor/git.rootprojects.org/root/keypairs/jws.go @@ -0,0 +1,63 @@ +package keypairs + +import ( + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "strings" +) + +// JWS is a parsed JWT, representation as signable/verifiable and human-readable parts +type JWS struct { + Header Object `json:"header"` // JSON + Claims Object `json:"claims"` // JSON + Protected string `json:"protected"` // base64 + Payload string `json:"payload"` // base64 + Signature string `json:"signature"` // base64 +} + +// JWSToJWT joins JWS parts into a JWT as {ProtectedHeader}.{SerializedPayload}.{Signature}. +func JWSToJWT(jwt *JWS) string { + return fmt.Sprintf( + "%s.%s.%s", + jwt.Protected, + jwt.Payload, + jwt.Signature, + ) +} + +// JWTToJWS splits the JWT into its JWS segments +func JWTToJWS(jwt string) (jws *JWS) { + jwt = strings.TrimSpace(jwt) + parts := strings.Split(jwt, ".") + if 3 != len(parts) { + return nil + } + return &JWS{ + Protected: parts[0], + Payload: parts[1], + Signature: parts[2], + } +} + +// DecodeComponents decodes JWS Header and Claims +func (jws *JWS) DecodeComponents() error { + protected, err := base64.RawURLEncoding.DecodeString(jws.Protected) + if nil != err { + return errors.New("invalid JWS header base64Url encoding") + } + if err := json.Unmarshal([]byte(protected), &jws.Header); nil != err { + return errors.New("invalid JWS header") + } + + payload, err := base64.RawURLEncoding.DecodeString(jws.Payload) + if nil != err { + return errors.New("invalid JWS payload base64Url encoding") + } + if err := json.Unmarshal([]byte(payload), &jws.Claims); nil != err { + return errors.New("invalid JWS claims") + } + + return nil +} diff --git a/vendor/git.rootprojects.org/root/keypairs/keyfetch/fetch.go b/vendor/git.rootprojects.org/root/keypairs/keyfetch/fetch.go new file mode 100644 index 0000000..c609531 --- /dev/null +++ b/vendor/git.rootprojects.org/root/keypairs/keyfetch/fetch.go @@ -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, " ") +} diff --git a/vendor/git.rootprojects.org/root/keypairs/keyfetch/uncached/fetch.go b/vendor/git.rootprojects.org/root/keypairs/keyfetch/uncached/fetch.go new file mode 100644 index 0000000..2e1c265 --- /dev/null +++ b/vendor/git.rootprojects.org/root/keypairs/keyfetch/uncached/fetch.go @@ -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:/// + 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, "/") + "/" +} diff --git a/vendor/git.rootprojects.org/root/keypairs/keypairs.go b/vendor/git.rootprojects.org/root/keypairs/keypairs.go new file mode 100644 index 0000000..ae6012b --- /dev/null +++ b/vendor/git.rootprojects.org/root/keypairs/keypairs.go @@ -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 +} diff --git a/vendor/git.rootprojects.org/root/keypairs/marshal.go b/vendor/git.rootprojects.org/root/keypairs/marshal.go new file mode 100644 index 0000000..2198c5e --- /dev/null +++ b/vendor/git.rootprojects.org/root/keypairs/marshal.go @@ -0,0 +1,171 @@ +package keypairs + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rsa" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "fmt" + "log" + "math/big" + mathrand "math/rand" +) + +// MarshalPEMPublicKey outputs the given public key as JWK +func MarshalPEMPublicKey(pubkey crypto.PublicKey) ([]byte, error) { + block, err := marshalDERPublicKey(pubkey) + if nil != err { + return nil, err + } + return pem.EncodeToMemory(block), nil +} + +// MarshalDERPublicKey outputs the given public key as JWK +func MarshalDERPublicKey(pubkey crypto.PublicKey) ([]byte, error) { + block, err := marshalDERPublicKey(pubkey) + if nil != err { + return nil, err + } + return block.Bytes, nil +} + +// marshalDERPublicKey outputs the given public key as JWK +func marshalDERPublicKey(pubkey crypto.PublicKey) (*pem.Block, error) { + + var der []byte + var typ string + var err error + switch k := pubkey.(type) { + case *rsa.PublicKey: + der = x509.MarshalPKCS1PublicKey(k) + typ = "RSA PUBLIC KEY" + case *ecdsa.PublicKey: + typ = "PUBLIC KEY" + der, err = x509.MarshalPKIXPublicKey(k) + if nil != err { + return nil, err + } + default: + panic("Developer Error: impossible key type") + } + + return &pem.Block{ + Bytes: der, + Type: typ, + }, nil +} + +// MarshalJWKPrivateKey outputs the given private key as JWK +func MarshalJWKPrivateKey(privkey 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(ErrInvalidPublicKey) + //return nil + } +} + +// MarshalDERPrivateKey outputs the given private key as ASN.1 DER +func MarshalDERPrivateKey(privkey PrivateKey) ([]byte, error) { + // thumbprint keys are alphabetically sorted and only include the necessary public parts + switch k := privkey.(type) { + case *rsa.PrivateKey: + return x509.MarshalPKCS1PrivateKey(k), nil + case *ecdsa.PrivateKey: + return x509.MarshalECPrivateKey(k) + default: + // this is unreachable because we know the types that we pass in + log.Printf("keytype: %t, %+v\n", privkey, privkey) + panic(ErrInvalidPublicKey) + //return nil, nil + } +} + +func marshalDERPrivateKey(privkey PrivateKey) (*pem.Block, error) { + var typ string + var bytes []byte + var err error + + switch k := privkey.(type) { + case *rsa.PrivateKey: + if 0 == mathrand.Intn(2) { + typ = "PRIVATE KEY" + bytes, err = x509.MarshalPKCS8PrivateKey(k) + if nil != err { + return nil, err + } + } else { + typ = "RSA PRIVATE KEY" + bytes = x509.MarshalPKCS1PrivateKey(k) + } + return &pem.Block{ + Type: typ, + Bytes: bytes, + }, nil + case *ecdsa.PrivateKey: + if 0 == mathrand.Intn(2) { + typ = "PRIVATE KEY" + bytes, err = x509.MarshalPKCS8PrivateKey(k) + } else { + typ = "EC PRIVATE KEY" + bytes, err = x509.MarshalECPrivateKey(k) + } + if nil != err { + return nil, err + } + return &pem.Block{ + Type: typ, + Bytes: bytes, + }, nil + default: + // this is unreachable because we know the types that we pass in + log.Printf("keytype: %t, %+v\n", privkey, privkey) + panic(ErrInvalidPublicKey) + //return nil, nil + } +} + +// MarshalPEMPrivateKey outputs the given private key as ASN.1 PEM +func MarshalPEMPrivateKey(privkey PrivateKey) ([]byte, error) { + block, err := marshalDERPrivateKey(privkey) + if nil != err { + return nil, err + } + return pem.EncodeToMemory(block), nil +} + +// 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, + )) +} diff --git a/vendor/git.rootprojects.org/root/keypairs/mock.go b/vendor/git.rootprojects.org/root/keypairs/mock.go new file mode 100644 index 0000000..2ca2a18 --- /dev/null +++ b/vendor/git.rootprojects.org/root/keypairs/mock.go @@ -0,0 +1,46 @@ +package keypairs + +import ( + "crypto/rsa" + "io" + "log" + mathrand "math/rand" +) + +// this shananigans is only for testing and debug API stuff +func (o *keyOptions) maybeMockReader() io.Reader { + if !allowMocking { + panic("mock method called when mocking is not allowed") + } + + if 0 == o.mockSeed { + return randReader + } + + log.Println("WARNING: MOCK: using insecure reader") + return mathrand.New(mathrand.NewSource(o.mockSeed)) +} + +const maxRetry = 16 + +func maybeDerandomizeMockKey(privkey PrivateKey, keylen int, opts *keyOptions) PrivateKey { + if 0 != opts.mockSeed { + for i := 0; i < maxRetry; i++ { + otherkey, _ := rsa.GenerateKey(opts.nextReader(), keylen) + otherCmp := otherkey.D.Cmp(privkey.(*rsa.PrivateKey).D) + if 0 != otherCmp { + // There are two possible keys, choose the lesser D value + // See https://github.com/square/go-jose/issues/189 + if otherCmp < 0 { + privkey = otherkey + } + break + } + if maxRetry == i-1 { + log.Printf("error: coinflip landed on heads %d times", maxRetry) + } + } + } + + return privkey +} diff --git a/vendor/git.rootprojects.org/root/keypairs/sign.go b/vendor/git.rootprojects.org/root/keypairs/sign.go new file mode 100644 index 0000000..59117ef --- /dev/null +++ b/vendor/git.rootprojects.org/root/keypairs/sign.go @@ -0,0 +1,165 @@ +package keypairs + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rsa" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + mathrand "math/rand" // to be used for good, not evil + "time" +) + +// Object is a type alias representing generic JSON data +type Object = map[string]interface{} + +// SignClaims adds `typ`, `kid` (or `jwk`), and `alg` in the header and expects claims for `jti`, `exp`, `iss`, and `iat` +func SignClaims(privkey PrivateKey, header Object, claims Object) (*JWS, error) { + var randsrc io.Reader = randReader + seed, _ := header["_seed"].(int64) + if 0 != seed { + randsrc = mathrand.New(mathrand.NewSource(seed)) + //delete(header, "_seed") + } + + protected, header, err := headerToProtected(NewPublicKey(privkey.Public()), header) + if nil != err { + return nil, err + } + protected64 := base64.RawURLEncoding.EncodeToString(protected) + + payload, err := claimsToPayload(claims) + if nil != err { + return nil, err + } + payload64 := base64.RawURLEncoding.EncodeToString(payload) + + signable := fmt.Sprintf(`%s.%s`, protected64, payload64) + hash := sha256.Sum256([]byte(signable)) + + sig := Sign(privkey, hash[:], randsrc) + sig64 := base64.RawURLEncoding.EncodeToString(sig) + //log.Printf("\n(Sign)\nSignable: %s", signable) + //log.Printf("Hash: %s", hash) + //log.Printf("Sig: %s", sig64) + + return &JWS{ + Header: header, + Claims: claims, + Protected: protected64, + Payload: payload64, + Signature: sig64, + }, nil +} + +func headerToProtected(pub PublicKey, header Object) ([]byte, Object, error) { + if nil == header { + header = Object{} + } + + // Only supporting 2048-bit and P256 keys right now + // because that's all that's practical and well-supported. + // No security theatre here. + alg := "ES256" + switch pub.Key().(type) { + case *rsa.PublicKey: + alg = "RS256" + } + + if selfSign, _ := header["_jwk"].(bool); selfSign { + delete(header, "_jwk") + any := Object{} + _ = json.Unmarshal(MarshalJWKPublicKey(pub), &any) + header["jwk"] = any + } + + // TODO what are the acceptable values? JWT. JWS? others? + header["typ"] = "JWT" + if _, ok := header["jwk"]; !ok { + thumbprint := ThumbprintPublicKey(pub) + kid, _ := header["kid"].(string) + if "" != kid && thumbprint != kid { + return nil, nil, errors.New("'kid' should be the key's thumbprint") + } + header["kid"] = thumbprint + } + header["alg"] = alg + + protected, err := json.Marshal(header) + if nil != err { + return nil, nil, err + } + return protected, header, nil +} + +func claimsToPayload(claims Object) ([]byte, error) { + if nil == claims { + claims = Object{} + } + + var dur time.Duration + jti, _ := claims["jti"].(string) + insecure, _ := claims["insecure"].(bool) + + switch exp := claims["exp"].(type) { + case time.Duration: + // TODO: MUST this go first? + // int64(time.Duration) vs time.Duration(int64) + dur = exp + case string: + var err error + dur, err = time.ParseDuration(exp) + // TODO s, err := time.ParseDuration(dur) + if nil != err { + return nil, err + } + case int: + dur = time.Second * time.Duration(exp) + case int64: + dur = time.Second * time.Duration(exp) + case float64: + dur = time.Second * time.Duration(exp) + default: + dur = 0 + } + + if "" == jti && 0 == dur && !insecure { + return nil, errors.New("token must have jti or exp as to be expirable / cancellable") + } + claims["exp"] = time.Now().Add(dur).Unix() + + return json.Marshal(claims) +} + +// Sign signs both RSA and ECDSA. Use `nil` or `crypto/rand.Reader` except for debugging. +func Sign(privkey PrivateKey, hash []byte, rand io.Reader) []byte { + if nil == rand { + rand = randReader + } + var sig []byte + + if len(hash) != 32 { + panic("only 256-bit hashes for 2048-bit and 256-bit keys are supported") + } + + switch k := privkey.(type) { + case *rsa.PrivateKey: + sig, _ = rsa.SignPKCS1v15(rand, k, crypto.SHA256, hash) + case *ecdsa.PrivateKey: + r, s, _ := ecdsa.Sign(rand, k, hash[:]) + rb := r.Bytes() + for len(rb) < 32 { + rb = append([]byte{0}, rb...) + } + sb := s.Bytes() + for len(rb) < 32 { + sb = append([]byte{0}, sb...) + } + sig = append(rb, sb...) + } + return sig +} diff --git a/vendor/git.rootprojects.org/root/keypairs/verify.go b/vendor/git.rootprojects.org/root/keypairs/verify.go new file mode 100644 index 0000000..f6dfae9 --- /dev/null +++ b/vendor/git.rootprojects.org/root/keypairs/verify.go @@ -0,0 +1,174 @@ +package keypairs + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rsa" + "crypto/sha256" + "crypto/subtle" + "encoding/base64" + "errors" + "fmt" + "log" + "math/big" + "time" +) + +// VerifyClaims will check the signature of a parsed JWT +func VerifyClaims(pubkey PublicKey, jws *JWS) (errs []error) { + kid, _ := jws.Header["kid"].(string) + jwkmap, hasJWK := jws.Header["jwk"].(Object) + //var jwk JWK = nil + + seed, _ := jws.Header["_seed"].(int64) + seedf64, _ := jws.Header["_seed"].(float64) + kty, _ := jws.Header["_kty"].(string) + if 0 == seed { + seed = int64(seedf64) + } + + var pub PublicKey = nil + if hasJWK { + pub, errs = selfsignCheck(jwkmap, errs) + } else { + opts := &keyOptions{mockSeed: seed, KeyType: kty} + pub, errs = pubkeyCheck(pubkey, kid, opts, errs) + } + + jti, _ := jws.Claims["jti"].(string) + expf64, _ := jws.Claims["exp"].(float64) + exp := int64(expf64) + if 0 == exp { + if "" == jti { + err := errors.New("one of 'jti' or 'exp' must exist for token expiry") + errs = append(errs, err) + } + } else { + if time.Now().Unix() > exp { + err := fmt.Errorf("token expired at %d (%s)", exp, time.Unix(exp, 0)) + errs = append(errs, err) + } + } + + signable := fmt.Sprintf("%s.%s", jws.Protected, jws.Payload) + hash := sha256.Sum256([]byte(signable)) + sig, err := base64.RawURLEncoding.DecodeString(jws.Signature) + if nil != err { + err := fmt.Errorf("could not decode signature: %w", err) + errs = append(errs, err) + return errs + } + + //log.Printf("\n(Verify)\nSignable: %s", signable) + //log.Printf("Hash: %s", hash) + //log.Printf("Sig: %s", jws.Signature) + if nil == pub { + err := fmt.Errorf("token signature could not be verified") + errs = append(errs, err) + } else if !Verify(pub, hash[:], sig) { + err := fmt.Errorf("token signature is not valid") + errs = append(errs, err) + } + return errs +} + +func selfsignCheck(jwkmap Object, errs []error) (PublicKey, []error) { + var pub PublicKey = nil + log.Println("Security TODO: did not check jws.Claims[\"sub\"] against 'jwk'") + log.Println("Security TODO: did not check jws.Claims[\"iss\"]") + kty := jwkmap["kty"] + var err error + if "RSA" == kty { + e, _ := jwkmap["e"].(string) + n, _ := jwkmap["n"].(string) + k, _ := (&RSAJWK{ + Exp: e, + N: n, + }).marshalJWK() + pub, err = ParseJWKPublicKey(k) + if nil != err { + return nil, append(errs, err) + } + } else { + crv, _ := jwkmap["crv"].(string) + x, _ := jwkmap["x"].(string) + y, _ := jwkmap["y"].(string) + k, _ := (&ECJWK{ + Curve: crv, + X: x, + Y: y, + }).marshalJWK() + pub, err = ParseJWKPublicKey(k) + if nil != err { + return nil, append(errs, err) + } + } + + return pub, errs +} + +func pubkeyCheck(pubkey PublicKey, kid string, opts *keyOptions, errs []error) (PublicKey, []error) { + var pub PublicKey = nil + + if "" == kid { + err := errors.New("token should have 'kid' or 'jwk' in header to identify the public key") + errs = append(errs, err) + } + + if nil == pubkey { + if allowMocking { + if 0 == opts.mockSeed { + err := errors.New("the debug API requires '_seed' to accompany 'kid'") + errs = append(errs, err) + } + if "" == opts.KeyType { + err := errors.New("the debug API requires '_kty' to accompany '_seed'") + errs = append(errs, err) + } + + if 0 == opts.mockSeed || "" == opts.KeyType { + return nil, errs + } + privkey := newPrivateKey(opts) + pub = NewPublicKey(privkey.Public()) + return pub, errs + } + err := errors.New("no matching public key") + errs = append(errs, err) + } else { + pub = pubkey + } + + if nil != pub && "" != kid { + if 1 != subtle.ConstantTimeCompare([]byte(kid), []byte(pub.Thumbprint())) { + err := errors.New("'kid' does not match the public key thumbprint") + errs = append(errs, err) + } + } + return pub, errs +} + +// Verify will check the signature of a hash +func Verify(pubkey PublicKey, hash []byte, sig []byte) bool { + + switch pub := pubkey.Key().(type) { + case *rsa.PublicKey: + //log.Printf("RSA VERIFY") + // TODO Size(key) to detect key size ? + //alg := "SHA256" + // TODO: this hasn't been tested yet + if err := rsa.VerifyPKCS1v15(pub, crypto.SHA256, hash, sig); nil != err { + return false + } + return true + case *ecdsa.PublicKey: + r := &big.Int{} + r.SetBytes(sig[0:32]) + s := &big.Int{} + s.SetBytes(sig[32:]) + return ecdsa.Verify(pub, hash, r, s) + default: + panic("impossible condition: non-rsa/non-ecdsa key") + //return false + } +} diff --git a/vendor/modules.txt b/vendor/modules.txt new file mode 100644 index 0000000..0440c19 --- /dev/null +++ b/vendor/modules.txt @@ -0,0 +1,5 @@ +# git.rootprojects.org/root/keypairs v0.6.5 +## explicit; go 1.12 +git.rootprojects.org/root/keypairs +git.rootprojects.org/root/keypairs/keyfetch +git.rootprojects.org/root/keypairs/keyfetch/uncached