go lint and update docs

This commit is contained in:
AJ ONeal 2020-04-10 13:59:44 -04:00
parent 78847a9cfd
commit 8f66f1d235
5 changed files with 121 additions and 58 deletions

View File

@ -1,54 +1,53 @@
# go-keypairs # go-keypairs
The lightest touch over top of Go's `crypto/ecdsa` and `crypto/rsa` to make them JSON Web Key (JWK) support and type safety lightly placed over top of Go's `crypto/ecdsa` and `crypto/rsa`
*typesafe* and to provide JSON Web Key (JWK) support.
# 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 # 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: I prefer to stay as close to Go's `crypto` package as possible,
just adding a light touch for JWT support and type safety.
> 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.
# Type Safety # Type Safety
Go has _excellent_ crytography support and provides wonderful `crypto.PublicKey` is a "marker interface", meaning that it is **not typesafe**!
primitives for dealing with them. Its Achilles' heel is they're **not typesafe**!
As of Go 1.11.5 `crypto.PublicKey` and `crypto.PrivateKey` are "marker interfaces" `go-keypairs` defines `type keypairs.PrivateKey interface { Public() crypto.PublicKey }`,
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 }`,
which is implemented by `crypto/rsa` and `crypto/ecdsa` which is implemented by `crypto/rsa` and `crypto/ecdsa`
(but not `crypto/dsa`, which we really don't care that much about). (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`, 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). 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 Although there are many, many ways that JWKs could be interpreted
(possibly why they haven't made it into the standard library), go-keypairs (possibly why they haven't made it into the standard library), `go-keypairs`
follows the basic pattern of `encoding/x509` to Parse and Marshal follows the basic pattern of `encoding/x509` to `Parse` and `Marshal`
only the most basic and most meaningful parts of a key. only the most basic and most meaningful parts of a key.
I highly recommend that you use `Thumbprint()` for `KeyID` you also 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 # LICENSE
Copyright (c) 2020-present AJ ONeal
Copyright (c) 2018-2019 Big Squid, Inc. Copyright (c) 2018-2019 Big Squid, Inc.
This work is licensed under the terms of the MIT license. This work is licensed under the terms of the MIT license.

View File

@ -23,11 +23,23 @@ import (
"github.com/big-squid/go-keypairs/keyfetch/uncached" "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") var EInvalidJWKURL = errors.New("url does not lead to valid JWKs")
// KeyCache is an in-memory key cache
var KeyCache = map[string]CachableKey{} var KeyCache = map[string]CachableKey{}
// KeyCacheMux is used to guard the in-memory cache
var KeyCacheMux = sync.Mutex{} 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") 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 { type CachableKey struct {
Key keypairs.PublicKey Key keypairs.PublicKey
Expiry time.Time Expiry time.Time
@ -55,50 +67,64 @@ type ID interface {
} }
*/ */
// StaleTime defines when public keys should be renewed (15 minutes by default)
var StaleTime = 15 * time.Minute var StaleTime = 15 * time.Minute
// DefaultKeyDuration defines how long a key should be considered fresh (48 hours by default)
var DefaultKeyDuration = 48 * time.Hour var DefaultKeyDuration = 48 * time.Hour
// MinimumKeyDuration defines the minimum time that a key will be cached (1 hour by default)
var MinimumKeyDuration = time.Hour var MinimumKeyDuration = time.Hour
// MaximumKeyDuration defines the maximum time that a key will be cached (72 hours by default)
var MaximumKeyDuration = 72 * time.Hour 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). // OIDCJWKs fetches baseURL + ".well-known/openid-configuration" and then fetches and returns the Public Keys.
func OIDCJWKs(baseURL string) (publicKeysMap, error) { func OIDCJWKs(baseURL string) (PublicKeysMap, error) {
if maps, keys, err := uncached.OIDCJWKs(baseURL); nil != err { maps, keys, err := uncached.OIDCJWKs(baseURL)
if nil != err {
return nil, err return nil, err
} else {
cacheKeys(maps, keys, baseURL)
return keys, 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) { func OIDCJWK(kidOrThumb, iss string) (keypairs.PublicKey, error) {
return immediateOneOrFetch(kidOrThumb, iss, uncached.OIDCJWKs) return immediateOneOrFetch(kidOrThumb, iss, uncached.OIDCJWKs)
} }
func WellKnownJWKs(kidOrThumb, iss string) (publicKeysMap, error) { // WellKnownJWKs fetches baseURL + ".well-known/jwks.json" and caches and returns the keys
if maps, keys, err := uncached.WellKnownJWKs(iss); nil != err { func WellKnownJWKs(kidOrThumb, iss string) (PublicKeysMap, error) {
maps, keys, err := uncached.WellKnownJWKs(iss)
if nil != err {
return nil, err return nil, err
} else {
cacheKeys(maps, keys, iss)
return keys, 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) { func WellKnownJWK(kidOrThumb, iss string) (keypairs.PublicKey, error) {
return immediateOneOrFetch(kidOrThumb, iss, uncached.WellKnownJWKs) return immediateOneOrFetch(kidOrThumb, iss, uncached.WellKnownJWKs)
} }
// JWKs returns a map of keys identified by their thumbprint // JWKs returns a map of keys identified by their thumbprint
// (since kid may or may not be present) // (since kid may or may not be present)
func JWKs(jwksurl string) (publicKeysMap, error) { func JWKs(jwksurl string) (PublicKeysMap, error) {
if maps, keys, err := uncached.JWKs(jwksurl); nil != err { maps, keys, err := uncached.JWKs(jwksurl)
if nil != err {
return nil, err return nil, err
} else {
iss := strings.Replace(jwksurl, ".well-known/jwks.json", "", 1)
cacheKeys(maps, keys, iss)
return keys, 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 // JWK tries to return a key from cache, falling back to the /.well-known/jwks.json of the issuer
@ -379,7 +405,7 @@ func hasImplicitTrust(issURL *url.URL, r *http.Request) bool {
return true return true
} }
// Whitelist // Whitelist is a newtype for an array of URLs
type Whitelist []*url.URL type Whitelist []*url.URL
// NewWhitelist turns an array of URLs (such as https://example.com/) into // NewWhitelist turns an array of URLs (such as https://example.com/) into

View File

@ -34,7 +34,7 @@ func TestIssuerMatches(t *testing.T) {
_, err := NewWhitelist(append(trusted, privates...)) _, err := NewWhitelist(append(trusted, privates...))
if nil == err { 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? // Empty list is allowed... I guess?

View File

@ -66,12 +66,13 @@ func JWKs(jwksurl string) (map[string]map[string]string, map[string]keypairs.Pub
k := resp.Keys[i] k := resp.Keys[i]
m := getStringMap(k) m := getStringMap(k)
if key, err := keypairs.NewJWKPublicKey(m); nil != err { key, err := keypairs.NewJWKPublicKey(m)
if nil != err {
return nil, nil, err return nil, nil, err
} else {
keys[key.Thumbprint()] = key
maps[key.Thumbprint()] = m
} }
keys[key.Thumbprint()] = key
maps[key.Thumbprint()] = m
} }
return maps, keys, nil return maps, keys, nil

View File

@ -21,18 +21,37 @@ import (
"time" "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") 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") 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") 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") 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") 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'") 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'") 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") 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") 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." 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." 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 // PrivateKey is a zero-cost typesafe substitue for crypto.PrivateKey
@ -63,34 +82,52 @@ type RSAPublicKey struct {
Expiry time.Time Expiry time.Time
} }
// Thumbprint returns a JWK thumbprint. See https://stackoverflow.com/questions/42588786/how-to-fingerprint-a-jwk
func (p *ECPublicKey) Thumbprint() string { func (p *ECPublicKey) Thumbprint() string {
return ThumbprintUntypedPublicKey(p.PublicKey) 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 { func (p *ECPublicKey) KeyID() string {
return p.KID return p.KID
} }
// Key returns the PublicKey
func (p *ECPublicKey) Key() crypto.PublicKey { func (p *ECPublicKey) Key() crypto.PublicKey {
return p.PublicKey return p.PublicKey
} }
// ExpireAt sets the time at which this Public Key should be considered invalid
func (p *ECPublicKey) ExpireAt(t time.Time) { func (p *ECPublicKey) ExpireAt(t time.Time) {
p.Expiry = t p.Expiry = t
} }
// ExpiresAt gets the time at which this Public Key should be considered invalid
func (p *ECPublicKey) ExpiresAt() time.Time { func (p *ECPublicKey) ExpiresAt() time.Time {
return p.Expiry return p.Expiry
} }
// Thumbprint returns a JWK thumbprint. See https://stackoverflow.com/questions/42588786/how-to-fingerprint-a-jwk
func (p *RSAPublicKey) Thumbprint() string { func (p *RSAPublicKey) Thumbprint() string {
return ThumbprintUntypedPublicKey(p.PublicKey) 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 { func (p *RSAPublicKey) KeyID() string {
return p.KID return p.KID
} }
// Key returns the PublicKey
func (p *RSAPublicKey) Key() crypto.PublicKey { func (p *RSAPublicKey) Key() crypto.PublicKey {
return p.PublicKey return p.PublicKey
} }
// ExpireAt sets the time at which this Public Key should be considered invalid
func (p *RSAPublicKey) ExpireAt(t time.Time) { func (p *RSAPublicKey) ExpireAt(t time.Time) {
p.Expiry = t p.Expiry = t
} }
// ExpiresAt gets the time at which this Public Key should be considered invalid
func (p *RSAPublicKey) ExpiresAt() time.Time { func (p *RSAPublicKey) ExpiresAt() time.Time {
return p.Expiry return p.Expiry
} }
@ -126,9 +163,9 @@ func NewPublicKey(pub crypto.PublicKey, kid ...string) PublicKey {
case *dsa.PublicKey: case *dsa.PublicKey:
panic(ErrInvalidPublicKey) panic(ErrInvalidPublicKey)
case *dsa.PrivateKey: case *dsa.PrivateKey:
panic(ErrInvalidPublicKey) panic(ErrInvalidPrivateKey)
default: default:
panic(errors.New(fmt.Sprintf(ErrDevBadKeyType, pub))) panic(fmt.Errorf(ErrDevBadKeyType, pub))
} }
return k return k
@ -236,7 +273,7 @@ func ParsePrivateKey(block []byte) (PrivateKey, error) {
// Parse PEM blocks (openssl generates junk metadata blocks for ECs) // Parse PEM blocks (openssl generates junk metadata blocks for ECs)
// or the original DER, or the JWK // or the original DER, or the JWK
for i, _ := range blocks { for i := range blocks {
block = blocks[i] block = blocks[i]
if key, err := parsePrivateKey(block); nil == err { if key, err := parsePrivateKey(block); nil == err {
return key, nil return key, nil
@ -320,9 +357,8 @@ func getPEMBytes(block []byte) ([][]byte, error) {
if len(blocks) > 0 { if len(blocks) > 0 {
return blocks, nil 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 // ParsePublicKey will try to parse the bytes you give it