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
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.

View File

@ -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

View File

@ -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?

View File

@ -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
}

View File

@ -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