AJ ONeal
2 years ago
20 changed files with 2303 additions and 0 deletions
@ -0,0 +1,5 @@ |
|||||
|
module git.rootprojects.org/root/libauth |
||||
|
|
||||
|
go 1.18 |
||||
|
|
||||
|
require git.rootprojects.org/root/keypairs v0.6.5 |
@ -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= |
@ -0,0 +1,5 @@ |
|||||
|
/keypairs |
||||
|
/dist/ |
||||
|
|
||||
|
.DS_Store |
||||
|
.*.sw* |
@ -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:' |
@ -0,0 +1 @@ |
|||||
|
AJ ONeal <aj@therootcompany.com> (https://therootcompany.com) |
@ -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 @@ |
|||||
|
# [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 <https://pkg.go.dev/git.rootprojects.org/root/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,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 |
@ -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,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 |
||||
|
} |
@ -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()), |
||||
|
} |
||||
|
} |
||||
|
*/ |
@ -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 |
||||
|
} |
@ -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,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, |
||||
|
)) |
||||
|
} |
@ -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 |
||||
|
} |
@ -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 |
||||
|
} |
@ -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
|
||||
|
} |
||||
|
} |
@ -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 |
Loading…
Reference in new issue