2
0
mirror of https://github.com/therootcompany/keypairs synced 2025-12-19 11:48:41 +00:00

Compare commits

..

No commits in common. "master" and "v0.6.4" have entirely different histories.

12 changed files with 126 additions and 378 deletions

View File

@ -1,41 +0,0 @@
# 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:'

View File

@ -1,44 +1,5 @@
# [keypairs](https://git.rootprojects.org/root/keypairs)
A cross-platform Command Line Tool and Golang Library that works
with RSA, ECDSA, PEM, DER, JWK, and the JOSE suite.
# Keypairs CLI
Generates, signs, and verifies with NIST-strength asymmetric keys.
```bash
# Generate JSON Web Keys (JWKs)
keypairs gen > key.jwk.json 2> pub.jwk.json
# Generate PEM (or DER) Keys, by extension
keypairs gen --key key.pem --pub pub.pem
# Sign a payload
keypairs sign key.jwk.json --exp 1h '{ "sub": "me@example.com" }' > token.jwt 2> sig.jws
# Verify a signature
keypairs verify pub.jwk.json token.jwt
```
Cheat Sheet at <https://webinstall.dev/keypairs>.
### Install
**Mac**, **Linux**:
```bash
curl -sS https://webinstall.dev/keypairs | bash
```
**Windows 10**:
```bash
curl.exe -A MS https://webinstall.dev/keypairs | powershell
```
# Keypairs Go Library
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.

View File

@ -10,7 +10,6 @@ import (
"time"
"git.rootprojects.org/root/keypairs"
"git.rootprojects.org/root/keypairs/keyfetch"
)
var (
@ -21,8 +20,7 @@ var (
)
func usage() {
fmt.Println(ver())
fmt.Println()
ver()
fmt.Println("Usage")
fmt.Printf(" %s <command> [flags] args...\n", name)
fmt.Println("")
@ -32,32 +30,29 @@ func usage() {
fmt.Println(" version")
fmt.Println(" gen")
fmt.Println(" sign")
fmt.Println(" inspect (decode)")
fmt.Println(" verify")
fmt.Println("")
fmt.Println("Examples:")
fmt.Println(" keypairs gen --key key.jwk.json [--pub <public-key>]")
fmt.Println(" keypairs gen -o key.jwk.json [--pub <public-key>]")
fmt.Println("")
fmt.Println(" keypairs sign --exp 15m key.jwk.json payload.json")
fmt.Println(" keypairs sign --exp 15m key.jwk.json '{ \"sub\": \"xxxx\" }'")
fmt.Println("")
fmt.Println(" keypairs inspect --verbose 'xxxx.yyyy.zzzz'")
fmt.Println("")
fmt.Println(" keypairs verify ./pub.jwk.json 'xxxx.yyyy.zzzz'")
// TODO fmt.Println(" keypairs verify --issuer https://example.com '{ \"sub\": \"xxxx\" }'")
fmt.Println("")
}
func ver() string {
return fmt.Sprintf("%s v%s (%s) %s", name, version, commit[:7], date)
func ver() {
fmt.Printf("%s v%s %s (%s)\n", name, version, commit[:7], date)
}
func main() {
args := os.Args[:]
if len(args) < 2 || "help" == args[1] {
if "help" == args[1] {
// top-level help
if len(args) <= 2 {
if 2 == len(args) {
usage()
os.Exit(0)
return
@ -70,17 +65,13 @@ func main() {
switch args[1] {
case "version":
fmt.Println(ver())
ver()
os.Exit(0)
return
case "gen":
gen(args[2:])
case "sign":
sign(args[2:])
case "decode":
fallthrough
case "inspect":
inspect(args[2:])
case "verify":
verify(args[2:])
default:
@ -92,28 +83,15 @@ func main() {
func gen(args []string) {
var keyname string
var keynameAlt string
//var keynameAlt2 string
var pubname string
flags := flag.NewFlagSet("gen", flag.ExitOnError)
flags.StringVar(&keynameAlt, "o", "", "output file (alias of --key)")
//flags.StringVar(&keynameAlt2, "priv", "", "private key file (alias of --key)")
flags.StringVar(&keyname, "key", "", "private key file (ex: key.jwk.json or key.pem)")
flags.StringVar(&keyname, "o", "", "private key file (ex: key.jwk.json or key.pem)")
flags.StringVar(&pubname, "pub", "", "public key file (ex: pub.jwk.json or pub.pem)")
flags.Parse(args)
if 0 == len(keyname) {
keyname = keynameAlt
}
/*
if 0 == len(keyname) {
keyname = keynameAlt2
}
*/
key := keypairs.NewDefaultPrivateKey()
marshalPriv(key, keyname)
pub := key.Public().(keypairs.PublicKey)
pub := keypairs.NewPublicKey(key.Public())
marshalPub(pub, pubname)
}
@ -184,109 +162,19 @@ func sign(args []string) {
fmt.Fprintf(os.Stdout, "%s\n", keypairs.JWSToJWT(jws))
}
func inspect(args []string) {
var verbose bool
flags := flag.NewFlagSet("inspect", flag.ExitOnError)
flags.BoolVar(&verbose, "verbose", true, "print extra info")
flags.Usage = func() {
fmt.Println("Usage: keypairs inspect --verbose <jwt-or-jwt>")
fmt.Println("")
fmt.Println(" <jwt-or-jws>: a JWT or JWS File or String, if JWS the payload must be Base64")
fmt.Println("")
}
flags.Parse(args)
if len(flags.Args()) < 1 {
flags.Usage()
os.Exit(1)
return
}
payload := flags.Args()[0]
jws, err := readJWS(payload)
if nil != err {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
return
}
var pub keypairs.PublicKey = nil
// because interfaces are never truly nil
hasPub := false
jwk, _ := jws.Header["jwk"].(map[string]interface{})
jwkE, _ := jwk["e"].(string)
jwkX, _ := jwk["x"].(string)
kid, _ := jws.Header["kid"].(string)
if len(jwkE) > 0 || len(jwkX) > 0 {
// TODO verify self-signed certificate
//b, _ := json.MarshalIndent(&jwk, "", " ")
if len(kid) > 0 {
fmt.Fprintf(os.Stderr, "[warn] jws header has both 'kid' (Key ID) and 'jwk' (for self-signed only)\n")
} else {
fmt.Fprintf(os.Stderr, "[debug] token is self-signed (jwk)\n")
//pub = pubx
//hasPub = true
}
} else if len(kid) > 0 {
iss, _ := jws.Claims["iss"].(string)
if strings.HasPrefix(iss, "http:") || strings.HasPrefix(iss, "https:") {
//fmt.Printf("iss: %s\n", iss)
//fmt.Printf("kid: %s\n", kid)
fmt.Fprintf(os.Stderr, "Checking for OIDC key... ")
pubx, err := keyfetch.OIDCJWK(kid, iss)
if nil != err {
fmt.Fprintf(os.Stderr, "not found.\n")
// ignore
} else {
fmt.Fprintf(os.Stderr, "found:\n")
if verbose {
b := keypairs.MarshalJWKPublicKey(pubx)
fmt.Fprintf(os.Stderr, "%s\n", indentJSON(b))
}
pub = pubx
hasPub = true
}
}
}
validSig := false
if hasPub {
errs := keypairs.VerifyClaims(pub, jws)
if len(errs) > 0 {
fmt.Fprintf(os.Stderr, "error:\n")
for _, err := range errs {
fmt.Fprintf(os.Stderr, "\t%v\n", err)
}
} else {
validSig = true
}
}
b, _ := json.MarshalIndent(&jws, "", " ")
fmt.Fprintf(os.Stdout, "%s\n", b)
if validSig {
fmt.Fprintf(os.Stderr, "Signature is Valid\n")
}
}
func verify(args []string) {
flags := flag.NewFlagSet("verify", flag.ExitOnError)
flags.Usage = func() {
fmt.Println("Usage: keypairs verify [public key] <jwt-or-jwt>")
fmt.Println("Usage: keypairs verify <public key> <jwt-or-jwt>")
fmt.Println("")
fmt.Println(" <public key>: a File or String of an EC or RSA key in JWK or PEM format")
fmt.Println(" <jwt-or-jws>: a JWT or JWS File or String, if JWS the payload must be Base64")
fmt.Println("")
}
flags.Parse(args)
if len(flags.Args()) < 1 {
if len(flags.Args()) <= 1 {
flags.Usage()
os.Exit(1)
return
}
if 1 == len(flags.Args()) {
inspect(args)
return
}
pubname := flags.Args()[0]
@ -360,26 +248,26 @@ func readPub(pubname string) (keypairs.PublicKey, error) {
b, err := ioutil.ReadFile(pubname)
if nil != err {
// No file? Try as string!
pub2, err2 := keypairs.ParsePublicKey([]byte(pubname))
var err2 error
pub, err2 = keypairs.ParsePublicKey([]byte(pubname))
if nil != err2 {
return nil, fmt.Errorf(
"could not read public key as file (or parse as string) %q:\n%w",
pubname, err,
)
}
pub = pub2.Key()
}
// Oh, it was a file.
if nil == pub {
pub3, err3 := keypairs.ParsePublicKey(b)
var err3 error
pub, err3 = keypairs.ParsePublicKey(b)
if nil != err3 {
return nil, fmt.Errorf(
"could not parse public key from file %q:\n%w",
pubname, err3,
)
}
pub = pub3.Key()
}
return pub, nil

View File

@ -25,8 +25,8 @@ import (
// TODO should be ErrInvalidJWKURL
// ErrInvalidJWKURL means that the url did not provide JWKs
var ErrInvalidJWKURL = errors.New("url does not lead to valid JWKs")
// 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{}
@ -41,7 +41,7 @@ var ErrInsecureDomain = errors.New("Whitelists should only allow secure URLs (i.
// CachableKey represents
type CachableKey struct {
Key keypairs.PublicKeyDeprecated
Key keypairs.PublicKey
Expiry time.Time
}
@ -80,7 +80,7 @@ var MinimumKeyDuration = time.Hour
var MaximumKeyDuration = 72 * time.Hour
// PublicKeysMap is a newtype for a map of keypairs.PublicKey
type PublicKeysMap = map[string]keypairs.PublicKeyDeprecated
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) {
@ -135,31 +135,20 @@ func JWK(kidOrThumb, iss string) (keypairs.PublicKey, error) {
// 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.PublicKeyDeprecated, error) {
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
}
pubd := keypairs.NewPublicKey(key)
// TODO bring this back
switch p := pubd.(type) {
case *keypairs.ECPublicKey:
p.KID = url
case *keypairs.RSAPublicKey:
p.KID = url
default:
return nil, nil, errors.New("impossible key type")
}
// put in a map, just for caching
maps := map[string]map[string]string{}
maps[keypairs.Thumbprint(key)] = m
maps[key.Thumbprint()] = m
maps[url] = m
keys := uncached.PublicKeysMap{} // map[string]keypairs.PublicKeyDeprecated{}
keys[keypairs.Thumbprint(key)] = pubd
keys[url] = pubd
keys := map[string]keypairs.PublicKey{}
keys[key.Thumbprint()] = key
keys[url] = key
return maps, keys, nil
})
@ -168,8 +157,7 @@ func PEM(url string) (keypairs.PublicKey, error) {
// 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.PublicKeyDeprecated, error) {
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
@ -177,10 +165,10 @@ func Fetch(url string) (keypairs.PublicKey, error) {
// put in a map, just for caching
maps := map[string]map[string]string{}
maps[keypairs.Thumbprint(key.Key())] = m
maps[key.Thumbprint()] = m
keys := map[string]keypairs.PublicKeyDeprecated{}
keys[keypairs.Thumbprint(key.Key())] = key
keys := map[string]keypairs.PublicKey{}
keys[key.Thumbprint()] = key
return maps, keys, nil
})
@ -190,7 +178,7 @@ func Fetch(url string) (keypairs.PublicKey, 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.Key()
return pub.Key
}
return nil
}
@ -224,21 +212,21 @@ func get(kidOrThumb, iss string) *CachableKey {
func immediateOneOrFetch(kidOrThumb, iss string, fetcher myfetcher) (keypairs.PublicKey, error) {
now := time.Now()
hit := get(kidOrThumb, iss)
key := get(kidOrThumb, iss)
if nil == hit {
if nil == key {
return fetchAndSelect(kidOrThumb, iss, fetcher)
}
// Fetch just a little before the key actually expires
if hit.Expiry.Sub(now) <= StaleTime {
if key.Expiry.Sub(now) <= StaleTime {
go fetchAndSelect(kidOrThumb, iss, fetcher)
}
return hit.Key.Key(), nil
return key.Key, nil
}
type myfetcher func(string) (map[string]map[string]string, map[string]keypairs.PublicKeyDeprecated, error)
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)
@ -249,21 +237,20 @@ func fetchAndSelect(id, baseURL string, fetcher myfetcher) (keypairs.PublicKey,
for i := range keys {
key := keys[i]
pub := key.Key()
if id == keypairs.Thumbprint(pub) {
return pub, nil
if id == key.Thumbprint() {
return key, nil
}
if id == key.KeyID() {
return pub, nil
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 PublicKeysMap, issuer string) {
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]
@ -273,13 +260,10 @@ func cacheKeys(maps map[string]map[string]string, keys PublicKeysMap, issuer str
}
iss = normalizeIssuer(iss)
cacheKey(m["kid"], iss, m["exp"], key)
if 0 == len(m[uncached.URLishKey]) {
cacheKey(m[uncached.URLishKey], iss, m["exp"], key)
}
}
}
func cacheKey(kid, iss, expstr string, pub keypairs.PublicKeyDeprecated) error {
func cacheKey(kid, iss, expstr string, pub keypairs.PublicKey) error {
var expiry time.Time
iss = normalizeIssuer(iss)
@ -303,7 +287,7 @@ func cacheKey(kid, iss, expstr string, pub keypairs.PublicKeyDeprecated) error {
Expiry: expiry,
}
// Since thumbprints are crypto secure, iss isn't needed
thumb := keypairs.Thumbprint(pub.Key())
thumb := pub.Thumbprint()
KeyCache[thumb] = CachableKey{
Key: pub,
Expiry: expiry,

View File

@ -11,19 +11,16 @@ import (
var pubkey keypairs.PublicKey
func TestCachesKey(t *testing.T) {
// TODO set KeyID() in cache
testCachesKey(t, "https://bigsquid.auth0.com/")
clear()
testCachesKey(t, "https://bigsquid.auth0.com")
// Get PEM
pubk3, err := PEM("https://bigsquid.auth0.com/pem")
k3, err := PEM("https://bigsquid.auth0.com/pem")
if nil != err {
t.Fatal("[0] Error fetching and caching key:", err)
t.Fatal("Error fetching and caching key:", err)
}
thumb3 := keypairs.Thumbprint(pubk3)
thumb := keypairs.Thumbprint(pubkey)
if thumb3 != thumb {
t.Fatalf("Error got different thumbprint for different versions of the same key %q != %q: %v", thumb3, thumb, err)
if k3.Thumbprint() != pubkey.Thumbprint() {
t.Fatal("Error got different thumbprint for different versions of the same key:", err)
}
clear()
testCachesKey(t, "https://big-squid.github.io/")
@ -50,10 +47,10 @@ func testCachesKey(t *testing.T, url string) {
var key keypairs.PublicKey
for i := range keys {
key = keys[i].Key()
key = keys[i]
break
}
thumb := keypairs.Thumbprint(key)
thumb := key.Thumbprint()
// Look in cache for each (and fail)
if pub := Get(thumb, ""); nil != pub {
@ -63,25 +60,20 @@ func testCachesKey(t *testing.T, url string) {
// Get with caching
pubkey, err = OIDCJWK(thumb, url)
if nil != err {
t.Fatal("[1] Error fetching and caching key:", err)
t.Fatal("Error fetching and caching key:", err)
}
// Look in cache for each (and succeed)
if pub := Get(thumb, ""); nil == pub {
t.Fatal("key was not properly cached by thumbprint", thumb)
}
// TODO thumb / id mapping
thumb = keypairs.Thumbprint(pubkey)
if pub := Get(thumb, url); nil == pub {
t.Fatal("key was not properly cached by kid", pubkey)
if "" != pubkey.KeyID() {
if pub := Get(pubkey.KeyID(), url); nil == pub {
t.Fatal("key was not properly cached by kid", pubkey.KeyID())
}
// TODO
/*
if 0 == len(keyfetch.GetID(thumb)) {
t.Log("Key did not have an explicit KeyID", thumb)
} else {
t.Log("Key did not have an explicit KeyID")
}
*/
// Get again (should be sub-ms instant)
now := time.Now()
@ -94,10 +86,8 @@ func testCachesKey(t *testing.T, url string) {
}
// Sanity check that the kid and thumb match
if !key.Equal(pubkey) || keypairs.Thumbprint(key) != keypairs.Thumbprint(pubkey) {
t.Fatalf("SANITY: [todo: KeyIDs or] Thumbprints do not match:\n%q != %q\n%q != %q",
keypairs.Thumbprint(key), keypairs.Thumbprint(pubkey),
keypairs.Thumbprint(key), keypairs.Thumbprint(pubkey))
if key.KeyID() != pubkey.KeyID() || key.Thumbprint() != pubkey.Thumbprint() {
t.Fatal("SANITY: KeyIDs or Thumbprints do not match:", key.KeyID(), pubkey.KeyID(), key.Thumbprint(), pubkey.Thumbprint())
}
// Get 404

View File

@ -4,8 +4,6 @@ package uncached
import (
"bytes"
"encoding/json"
"crypto/rsa"
"crypto/ecdsa"
"errors"
"io"
"io/ioutil"
@ -17,17 +15,8 @@ import (
"git.rootprojects.org/root/keypairs"
)
// URLishKey is TODO
var URLishKey = "_kid_url"
// JWKMapByID is TODO
type JWKMapByID = map[string]map[string]string
// PublicKeysMap is TODO
type PublicKeysMap = map[string]keypairs.PublicKeyDeprecated
// OIDCJWKs gets the OpenID Connect configuration from the baseURL and then calls JWKs with the specified jwks_uri
func OIDCJWKs(baseURL string) (JWKMapByID, PublicKeysMap, error) {
func OIDCJWKs(baseURL string) (map[string]map[string]string, map[string]keypairs.PublicKey, error) {
baseURL = normalizeBaseURL(baseURL)
oidcConf := struct {
JWKSURI string `json:"jwks_uri"`
@ -48,7 +37,7 @@ func OIDCJWKs(baseURL string) (JWKMapByID, PublicKeysMap, error) {
}
// WellKnownJWKs calls JWKs with baseURL + /.well-known/jwks.json as constructs the jwks_uri
func WellKnownJWKs(baseURL string) (JWKMapByID, PublicKeysMap, error) {
func WellKnownJWKs(baseURL string) (map[string]map[string]string, map[string]keypairs.PublicKey, error) {
baseURL = normalizeBaseURL(baseURL)
url := baseURL + ".well-known/jwks.json"
@ -56,9 +45,9 @@ func WellKnownJWKs(baseURL string) (JWKMapByID, PublicKeysMap, error) {
}
// JWKs fetches and parses a jwks.json (assuming well-known format)
func JWKs(jwksurl string) (JWKMapByID, PublicKeysMap, error) {
keys := PublicKeysMap{}
maps := JWKMapByID{}
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"`
}{
@ -82,8 +71,8 @@ func JWKs(jwksurl string) (JWKMapByID, PublicKeysMap, error) {
if nil != err {
return nil, nil, err
}
keys[keypairs.Thumbprint(key.Key())] = key
maps[keypairs.Thumbprint(key.Key())] = m
keys[key.Thumbprint()] = key
maps[key.Thumbprint()] = m
}
return maps, keys, nil
@ -91,38 +80,32 @@ func JWKs(jwksurl string) (JWKMapByID, PublicKeysMap, error) {
// PEM fetches and parses a PEM (assuming well-known format)
func PEM(pemurl string) (map[string]string, keypairs.PublicKey, error) {
var pubd keypairs.PublicKeyDeprecated
var pub keypairs.PublicKey
if err := safeFetch(pemurl, func(body io.Reader) error {
pem, err := ioutil.ReadAll(body)
if nil != err {
return err
}
pubd, err = keypairs.ParsePublicKey(pem)
if nil != err {
pub, err = keypairs.ParsePublicKey(pem)
return err
}
return nil
}); nil != err {
return nil, nil, err
}
jwk := map[string]interface{}{}
pub := pubd.Key()
body := bytes.NewBuffer(keypairs.MarshalJWKPublicKey(pub))
decoder := json.NewDecoder(body)
decoder.UseNumber()
_ = decoder.Decode(&jwk)
m := getStringMap(jwk)
m["kid"] = keypairs.Thumbprint(pub)
// TODO is this just junk?
m[URLishKey] = pemurl
m["kid"] = pemurl
switch pub.(type) {
case *ecdsa.PublicKey:
//p.KID = pemurl
case *rsa.PublicKey:
//p.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")
}
@ -131,7 +114,7 @@ func PEM(pemurl string) (map[string]string, keypairs.PublicKey, error) {
}
// Fetch retrieves a single JWK (plain, bare jwk) from a URL (off-spec)
func Fetch(url string) (map[string]string, keypairs.PublicKeyDeprecated, error) {
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)

View File

@ -3,6 +3,7 @@ package keypairs
import (
"bytes"
"crypto"
"crypto/dsa"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rsa"
@ -56,21 +57,14 @@ const ErrDevBadKeyType = "[Developer Error] crypto.PublicKey and crypto.PrivateK
// PrivateKey is a zero-cost typesafe substitue for crypto.PrivateKey
type PrivateKey interface {
Public() crypto.PublicKey
Equal(x crypto.PrivateKey) bool
}
// PublicKey is so that v0.7.x can use golang v1.15 keys
// PublicKey thinly veils crypto.PublicKey for type safety
type PublicKey interface {
Equal(x crypto.PublicKey) bool
}
// PublicKeyDeprecated thinly veils crypto.PublicKey for type safety
type PublicKeyDeprecated interface {
crypto.PublicKey
//Equal(x crypto.PublicKey) bool
//Thumbprint() string
Thumbprint() string
KeyID() string
Key() PublicKey
Key() crypto.PublicKey
ExpiresAt() time.Time
}
@ -93,18 +87,13 @@ func (p *ECPublicKey) Thumbprint() string {
return ThumbprintUntypedPublicKey(p.PublicKey)
}
// Equal returns true if the public key is equal.
func (p *ECPublicKey) Equal(x crypto.PublicKey) bool {
return p.PublicKey.Equal(x)
}
// 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() PublicKey {
func (p *ECPublicKey) Key() crypto.PublicKey {
return p.PublicKey
}
@ -123,18 +112,13 @@ func (p *RSAPublicKey) Thumbprint() string {
return ThumbprintUntypedPublicKey(p.PublicKey)
}
// Equal returns true if the public key is equal.
func (p *RSAPublicKey) Equal(x crypto.PublicKey) bool {
return p.PublicKey.Equal(x)
}
// 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() PublicKey {
func (p *RSAPublicKey) Key() crypto.PublicKey {
return p.PublicKey
}
@ -149,13 +133,8 @@ func (p *RSAPublicKey) ExpiresAt() time.Time {
}
// NewPublicKey wraps a crypto.PublicKey to make it typesafe.
func NewPublicKey(pub crypto.PublicKey, kid ...string) PublicKeyDeprecated {
_, ok := pub.(PublicKey)
if !ok {
panic("Developer Error: not a crypto.PublicKey")
}
var k PublicKeyDeprecated
func NewPublicKey(pub crypto.PublicKey, kid ...string) PublicKey {
var k PublicKey
switch p := pub.(type) {
case *ecdsa.PublicKey:
eckey := &ECPublicKey{
@ -177,6 +156,14 @@ func NewPublicKey(pub crypto.PublicKey, kid ...string) PublicKeyDeprecated {
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))
}
@ -188,11 +175,13 @@ func NewPublicKey(pub crypto.PublicKey, kid ...string) PublicKeyDeprecated {
// 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.(type) {
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)
@ -200,13 +189,8 @@ func MarshalJWKPublicKey(key PublicKey, exp ...time.Time) []byte {
}
}
// Thumbprint returns the SHA256 RFC-spec JWK thumbprint
func Thumbprint(pub PublicKey) string {
return ThumbprintUntypedPublicKey(pub)
}
// ThumbprintPublicKey returns the SHA256 RFC-spec JWK thumbprint
func ThumbprintPublicKey(pub PublicKeyDeprecated) string {
func ThumbprintPublicKey(pub PublicKey) string {
return ThumbprintUntypedPublicKey(pub.Key())
}
@ -214,7 +198,7 @@ func ThumbprintPublicKey(pub PublicKeyDeprecated) string {
// (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 PublicKeyDeprecated:
case PublicKey:
return ThumbprintUntypedPublicKey(p.Key())
case *ecdsa.PublicKey:
return ThumbprintECPublicKey(p)
@ -379,7 +363,7 @@ func getPEMBytes(block []byte) ([][]byte, error) {
// 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) (PublicKeyDeprecated, error) {
func ParsePublicKey(block []byte) (PublicKey, error) {
blocks, err := getPEMBytes(block)
if nil != err {
return nil, ErrParsePublicKey
@ -406,11 +390,11 @@ func ParsePublicKey(block []byte) (PublicKeyDeprecated, error) {
}
// ParsePublicKeyString calls ParsePublicKey([]byte(key)) for all you lazy folk.
func ParsePublicKeyString(block string) (PublicKeyDeprecated, error) {
func ParsePublicKeyString(block string) (PublicKey, error) {
return ParsePublicKey([]byte(block))
}
func parsePublicKey(der []byte) (PublicKeyDeprecated, error) {
func parsePublicKey(der []byte) (PublicKey, error) {
cert, err := x509.ParseCertificate(der)
if nil == err {
switch k := cert.PublicKey.(type) {
@ -456,7 +440,7 @@ func parsePublicKey(der []byte) (PublicKeyDeprecated, error) {
}
// NewJWKPublicKey contstructs a PublicKey from the relevant pieces a map[string]string (generic JSON)
func NewJWKPublicKey(m map[string]string) (PublicKeyDeprecated, error) {
func NewJWKPublicKey(m map[string]string) (PublicKey, error) {
switch m["kty"] {
case "RSA":
return parseRSAPublicKey(m)
@ -468,7 +452,7 @@ func NewJWKPublicKey(m map[string]string) (PublicKeyDeprecated, error) {
}
// ParseJWKPublicKey parses a JSON-encoded JWK and returns a PublicKey, or a (hopefully) helpful error message
func ParseJWKPublicKey(b []byte) (PublicKeyDeprecated, error) {
func ParseJWKPublicKey(b []byte) (PublicKey, error) {
// RSA and EC have "d" as a private part
if bytes.Contains(b, []byte(`"d"`)) {
return nil, ErrUnexpectedPrivateKey
@ -477,7 +461,7 @@ func ParseJWKPublicKey(b []byte) (PublicKeyDeprecated, error) {
}
// ParseJWKPublicKeyString calls ParseJWKPublicKey([]byte(key)) for all you lazy folk.
func ParseJWKPublicKeyString(s string) (PublicKeyDeprecated, error) {
func ParseJWKPublicKeyString(s string) (PublicKey, error) {
if strings.Contains(s, `"d"`) {
return nil, ErrUnexpectedPrivateKey
}
@ -485,7 +469,7 @@ func ParseJWKPublicKeyString(s string) (PublicKeyDeprecated, error) {
}
// DecodeJWKPublicKey stream-decodes a JSON-encoded JWK and returns a PublicKey, or a (hopefully) helpful error message
func DecodeJWKPublicKey(r io.Reader) (PublicKeyDeprecated, error) {
func DecodeJWKPublicKey(r io.Reader) (PublicKey, error) {
m := make(map[string]string)
if err := json.NewDecoder(r).Decode(&m); nil != err {
return nil, err
@ -497,7 +481,7 @@ func DecodeJWKPublicKey(r io.Reader) (PublicKeyDeprecated, error) {
}
// the underpinnings of the parser as used by the typesafe wrappers
func newJWKPublicKey(data interface{}) (PublicKeyDeprecated, error) {
func newJWKPublicKey(data interface{}) (PublicKey, error) {
var m map[string]string
switch d := data.(type) {

View File

@ -39,7 +39,7 @@ var never = time.Time{}
// Middleware holds your public keys and has http handler methods for OIDC and Auth0 JWKs
type Middleware struct {
BaseURL *url.URL
Keys []keypairs.PublicKeyDeprecated
Keys []keypairs.PublicKey
ExpiresIn time.Duration
}
@ -148,7 +148,7 @@ func (m *Middleware) Auth0PEM(w http.ResponseWriter, r *http.Request) {
}
}
func marshalJWKs(keys []keypairs.PublicKeyDeprecated, exp2 time.Time) []string {
func marshalJWKs(keys []keypairs.PublicKey, exp2 time.Time) []string {
jwks := make([]string, 0, 1)
for i := range keys {
@ -163,8 +163,7 @@ func marshalJWKs(keys []keypairs.PublicKeyDeprecated, exp2 time.Time) []string {
// Note that you don't have to embed `iss` in the JWK because the client
// already has that info by virtue of getting to it in the first place.
pub := key.Key()
jwk := string(keypairs.MarshalJWKPublicKey(pub, exp))
jwk := string(keypairs.MarshalJWKPublicKey(key, exp))
jwks = append(jwks, jwk)
}

View File

@ -18,7 +18,7 @@ import (
func TestServeKeys(t *testing.T) {
eckey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
pubs := []keypairs.PublicKeyDeprecated{
pubs := []keypairs.PublicKey{
keypairs.NewPublicKey(eckey.Public()),
}
@ -42,9 +42,8 @@ func TestServeKeys(t *testing.T) {
go func() {
time.Sleep(15 * time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
ctx, _ := context.WithTimeout(context.Background(), 15*time.Second)
h.Shutdown(ctx)
cancel()
}()
m := map[string]string{}

View File

@ -1,6 +1,7 @@
package keypairs
import (
"crypto"
"crypto/ecdsa"
"crypto/rsa"
"crypto/x509"
@ -13,7 +14,7 @@ import (
)
// MarshalPEMPublicKey outputs the given public key as JWK
func MarshalPEMPublicKey(pubkey PublicKey) ([]byte, error) {
func MarshalPEMPublicKey(pubkey crypto.PublicKey) ([]byte, error) {
block, err := marshalDERPublicKey(pubkey)
if nil != err {
return nil, err
@ -22,7 +23,7 @@ func MarshalPEMPublicKey(pubkey PublicKey) ([]byte, error) {
}
// MarshalDERPublicKey outputs the given public key as JWK
func MarshalDERPublicKey(pubkey PublicKey) ([]byte, error) {
func MarshalDERPublicKey(pubkey crypto.PublicKey) ([]byte, error) {
block, err := marshalDERPublicKey(pubkey)
if nil != err {
return nil, err
@ -31,7 +32,7 @@ func MarshalDERPublicKey(pubkey PublicKey) ([]byte, error) {
}
// marshalDERPublicKey outputs the given public key as JWK
func marshalDERPublicKey(pubkey PublicKey) (*pem.Block, error) {
func marshalDERPublicKey(pubkey crypto.PublicKey) (*pem.Block, error) {
var der []byte
var typ string

View File

@ -26,7 +26,7 @@ func SignClaims(privkey PrivateKey, header Object, claims Object) (*JWS, error)
//delete(header, "_seed")
}
protected, header, err := headerToProtected(privkey.Public().(PublicKey), header)
protected, header, err := headerToProtected(NewPublicKey(privkey.Public()), header)
if nil != err {
return nil, err
}
@ -65,7 +65,7 @@ func headerToProtected(pub PublicKey, header Object) ([]byte, Object, error) {
// because that's all that's practical and well-supported.
// No security theatre here.
alg := "ES256"
switch pub.(type) {
switch pub.Key().(type) {
case *rsa.PublicKey:
alg = "RS256"
}
@ -80,7 +80,7 @@ func headerToProtected(pub PublicKey, header Object) ([]byte, Object, error) {
// TODO what are the acceptable values? JWT. JWS? others?
header["typ"] = "JWT"
if _, ok := header["jwk"]; !ok {
thumbprint := ThumbprintPublicKey(NewPublicKey(pub))
thumbprint := ThumbprintPublicKey(pub)
kid, _ := header["kid"].(string)
if "" != kid && thumbprint != kid {
return nil, nil, errors.New("'kid' should be the key's thumbprint")

View File

@ -73,7 +73,7 @@ func VerifyClaims(pubkey PublicKey, jws *JWS) (errs []error) {
}
func selfsignCheck(jwkmap Object, errs []error) (PublicKey, []error) {
var pub PublicKeyDeprecated = nil
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"]
@ -104,7 +104,7 @@ func selfsignCheck(jwkmap Object, errs []error) (PublicKey, []error) {
}
}
return pub.Key(), errs
return pub, errs
}
func pubkeyCheck(pubkey PublicKey, kid string, opts *keyOptions, errs []error) (PublicKey, []error) {
@ -130,7 +130,7 @@ func pubkeyCheck(pubkey PublicKey, kid string, opts *keyOptions, errs []error) (
return nil, errs
}
privkey := newPrivateKey(opts)
pub = privkey.Public().(PublicKey)
pub = NewPublicKey(privkey.Public())
return pub, errs
}
err := errors.New("no matching public key")
@ -140,7 +140,7 @@ func pubkeyCheck(pubkey PublicKey, kid string, opts *keyOptions, errs []error) (
}
if nil != pub && "" != kid {
if 1 != subtle.ConstantTimeCompare([]byte(kid), []byte(Thumbprint(pub))) {
if 1 != subtle.ConstantTimeCompare([]byte(kid), []byte(pub.Thumbprint())) {
err := errors.New("'kid' does not match the public key thumbprint")
errs = append(errs, err)
}
@ -151,7 +151,7 @@ func pubkeyCheck(pubkey PublicKey, kid string, opts *keyOptions, errs []error) (
// Verify will check the signature of a hash
func Verify(pubkey PublicKey, hash []byte, sig []byte) bool {
switch pub := pubkey.(type) {
switch pub := pubkey.Key().(type) {
case *rsa.PublicKey:
//log.Printf("RSA VERIFY")
// TODO Size(key) to detect key size ?