go lint and update docs
This commit is contained in:
parent
78847a9cfd
commit
8f66f1d235
58
README.md
58
README.md
|
@ -1,54 +1,53 @@
|
|||
# go-keypairs
|
||||
|
||||
The lightest touch over top of Go's `crypto/ecdsa` and `crypto/rsa` to make them
|
||||
*typesafe* and to provide JSON Web Key (JWK) support.
|
||||
JSON Web Key (JWK) support and type safety lightly placed over top of Go's `crypto/ecdsa` and `crypto/rsa`
|
||||
|
||||
# Documentation
|
||||
Useful for JWT, JOSE, etc.
|
||||
|
||||
Use the source, Luke!
|
||||
```go
|
||||
key, err := keypairs.ParsePrivateKey(bytesForJWKOrPEMOrDER)
|
||||
|
||||
<https://godoc.org/github.com/big-squid/go-keypairs>
|
||||
pub, err := keypairs.ParsePublicKey(bytesForJWKOrPEMOrDER)
|
||||
|
||||
jwk, err := keypairs.MarshalJWKPublicKey(pub, time.Now().Add(2 * time.Day))
|
||||
|
||||
kid, err := keypairs.ThumbprintPublicKey(pub)
|
||||
```
|
||||
|
||||
# API Documentation
|
||||
|
||||
See <https://godoc.org/github.com/big-squid/go-keypairs>
|
||||
|
||||
# Philosophy
|
||||
|
||||
Always remember:
|
||||
Go's standard library is great.
|
||||
|
||||
> Don't roll your own crypto.
|
||||
Go has _excellent_ crytography support and provides wonderful
|
||||
primitives for dealing with them.
|
||||
|
||||
But also remember:
|
||||
|
||||
> Just because you _don't_ know someone doesn't make them smart.
|
||||
|
||||
Don't get the two mixed up!
|
||||
|
||||
(furthermore, [just because you _do_ know someone doesn't make them _not_ smart](https://www.humancondition.com/asid-prophets-without-honour-in-their-own-home/))
|
||||
|
||||
Although I would **not** want to invent my own cryptographic algorithm,
|
||||
I've read enough source code to know that, for standards I know well,
|
||||
I feel much more confident in the security, extensibility, and documentation
|
||||
of tooling that I've write myself.
|
||||
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
|
||||
|
||||
Go has _excellent_ crytography support and provides wonderful
|
||||
primitives for dealing with them. Its Achilles' heel is they're **not typesafe**!
|
||||
`crypto.PublicKey` is a "marker interface", meaning that it is **not typesafe**!
|
||||
|
||||
As of Go 1.11.5 `crypto.PublicKey` and `crypto.PrivateKey` are "marker interfaces"
|
||||
or, in other words, empty interfaces that only serve to document intent without
|
||||
actually providing a constraint to the type system.
|
||||
|
||||
go-keypairs defines `type keypairs.PrivateKey interface { Public() crypto.PublicKey }`,
|
||||
`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 "codec"
|
||||
## 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
|
||||
(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
|
||||
|
@ -57,6 +56,7 @@ 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.
|
||||
|
|
|
@ -23,11 +23,23 @@ import (
|
|||
"github.com/big-squid/go-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
|
||||
|
@ -55,51 +67,65 @@ type ID interface {
|
|||
}
|
||||
*/
|
||||
|
||||
// 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
|
||||
|
||||
type publicKeysMap map[string]keypairs.PublicKey
|
||||
// PublicKeysMap is a newtype for a map of keypairs.PublicKey
|
||||
type PublicKeysMap map[string]keypairs.PublicKey
|
||||
|
||||
// FetchOIDCPublicKeys fetches baseURL + ".well-known/openid-configuration" and then returns FetchPublicKeys(jwks_uri).
|
||||
func OIDCJWKs(baseURL string) (publicKeysMap, error) {
|
||||
if maps, keys, err := uncached.OIDCJWKs(baseURL); nil != err {
|
||||
// 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
|
||||
} else {
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
func WellKnownJWKs(kidOrThumb, iss string) (publicKeysMap, error) {
|
||||
if maps, keys, err := uncached.WellKnownJWKs(iss); nil != err {
|
||||
// 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
|
||||
} else {
|
||||
}
|
||||
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) {
|
||||
if maps, keys, err := uncached.JWKs(jwksurl); nil != err {
|
||||
func JWKs(jwksurl string) (PublicKeysMap, error) {
|
||||
maps, keys, err := uncached.JWKs(jwksurl)
|
||||
|
||||
if nil != err {
|
||||
return nil, err
|
||||
} else {
|
||||
}
|
||||
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) {
|
||||
|
@ -379,7 +405,7 @@ func hasImplicitTrust(issURL *url.URL, r *http.Request) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
// Whitelist
|
||||
// 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
|
||||
|
|
|
@ -34,7 +34,7 @@ func TestIssuerMatches(t *testing.T) {
|
|||
|
||||
_, err := NewWhitelist(append(trusted, privates...))
|
||||
if nil == err {
|
||||
t.Fatal(errors.New("An insecure domain got through!"))
|
||||
t.Fatal(errors.New("an insecure domain got through"))
|
||||
}
|
||||
|
||||
// Empty list is allowed... I guess?
|
||||
|
|
|
@ -66,13 +66,14 @@ func JWKs(jwksurl string) (map[string]map[string]string, map[string]keypairs.Pub
|
|||
k := resp.Keys[i]
|
||||
m := getStringMap(k)
|
||||
|
||||
if key, err := keypairs.NewJWKPublicKey(m); nil != err {
|
||||
key, err := keypairs.NewJWKPublicKey(m)
|
||||
|
||||
if nil != err {
|
||||
return nil, nil, err
|
||||
} else {
|
||||
}
|
||||
keys[key.Thumbprint()] = key
|
||||
maps[key.Thumbprint()] = m
|
||||
}
|
||||
}
|
||||
|
||||
return maps, keys, nil
|
||||
}
|
||||
|
|
46
keypairs.go
46
keypairs.go
|
@ -21,18 +21,37 @@ import (
|
|||
"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
|
||||
|
@ -63,34 +82,52 @@ type RSAPublicKey struct {
|
|||
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
|
||||
}
|
||||
|
@ -126,9 +163,9 @@ func NewPublicKey(pub crypto.PublicKey, kid ...string) PublicKey {
|
|||
case *dsa.PublicKey:
|
||||
panic(ErrInvalidPublicKey)
|
||||
case *dsa.PrivateKey:
|
||||
panic(ErrInvalidPublicKey)
|
||||
panic(ErrInvalidPrivateKey)
|
||||
default:
|
||||
panic(errors.New(fmt.Sprintf(ErrDevBadKeyType, pub)))
|
||||
panic(fmt.Errorf(ErrDevBadKeyType, pub))
|
||||
}
|
||||
|
||||
return k
|
||||
|
@ -236,7 +273,7 @@ func ParsePrivateKey(block []byte) (PrivateKey, error) {
|
|||
|
||||
// Parse PEM blocks (openssl generates junk metadata blocks for ECs)
|
||||
// or the original DER, or the JWK
|
||||
for i, _ := range blocks {
|
||||
for i := range blocks {
|
||||
block = blocks[i]
|
||||
if key, err := parsePrivateKey(block); nil == err {
|
||||
return key, nil
|
||||
|
@ -320,9 +357,8 @@ func getPEMBytes(block []byte) ([][]byte, error) {
|
|||
|
||||
if len(blocks) > 0 {
|
||||
return blocks, nil
|
||||
} else {
|
||||
return nil, errors.New("no PEM blocks found")
|
||||
}
|
||||
return nil, errors.New("no PEM blocks found")
|
||||
}
|
||||
|
||||
// ParsePublicKey will try to parse the bytes you give it
|
||||
|
|
Loading…
Reference in New Issue