mirror of
https://github.com/therootcompany/keypairs
synced 2025-12-19 11:48:41 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 88a0b05c2f | |||
| a62ae2ea87 | |||
| be38720eba | |||
| 286cf11070 | |||
| 743731c804 | |||
| 098f92178a | |||
| 2e3ead4102 | |||
| 7e6fd174ef | |||
| d4f18f8c78 | |||
| f275cd82d3 |
41
.goreleaser.yml
Normal file
41
.goreleaser.yml
Normal file
@ -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:'
|
||||||
39
README.md
39
README.md
@ -1,5 +1,44 @@
|
|||||||
# [keypairs](https://git.rootprojects.org/root/keypairs)
|
# [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`
|
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.
|
Useful for JWT, JOSE, etc.
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.rootprojects.org/root/keypairs"
|
"git.rootprojects.org/root/keypairs"
|
||||||
|
"git.rootprojects.org/root/keypairs/keyfetch"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -20,7 +21,8 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func usage() {
|
func usage() {
|
||||||
ver()
|
fmt.Println(ver())
|
||||||
|
fmt.Println()
|
||||||
fmt.Println("Usage")
|
fmt.Println("Usage")
|
||||||
fmt.Printf(" %s <command> [flags] args...\n", name)
|
fmt.Printf(" %s <command> [flags] args...\n", name)
|
||||||
fmt.Println("")
|
fmt.Println("")
|
||||||
@ -30,29 +32,32 @@ func usage() {
|
|||||||
fmt.Println(" version")
|
fmt.Println(" version")
|
||||||
fmt.Println(" gen")
|
fmt.Println(" gen")
|
||||||
fmt.Println(" sign")
|
fmt.Println(" sign")
|
||||||
|
fmt.Println(" inspect (decode)")
|
||||||
fmt.Println(" verify")
|
fmt.Println(" verify")
|
||||||
fmt.Println("")
|
fmt.Println("")
|
||||||
fmt.Println("Examples:")
|
fmt.Println("Examples:")
|
||||||
fmt.Println(" keypairs gen -o key.jwk.json [--pub <public-key>]")
|
fmt.Println(" keypairs gen --key key.jwk.json [--pub <public-key>]")
|
||||||
fmt.Println("")
|
fmt.Println("")
|
||||||
fmt.Println(" keypairs sign --exp 15m key.jwk.json payload.json")
|
fmt.Println(" keypairs sign --exp 15m key.jwk.json payload.json")
|
||||||
fmt.Println(" keypairs sign --exp 15m key.jwk.json '{ \"sub\": \"xxxx\" }'")
|
fmt.Println(" keypairs sign --exp 15m key.jwk.json '{ \"sub\": \"xxxx\" }'")
|
||||||
fmt.Println("")
|
fmt.Println("")
|
||||||
|
fmt.Println(" keypairs inspect --verbose 'xxxx.yyyy.zzzz'")
|
||||||
|
fmt.Println("")
|
||||||
fmt.Println(" keypairs verify ./pub.jwk.json 'xxxx.yyyy.zzzz'")
|
fmt.Println(" keypairs verify ./pub.jwk.json 'xxxx.yyyy.zzzz'")
|
||||||
// TODO fmt.Println(" keypairs verify --issuer https://example.com '{ \"sub\": \"xxxx\" }'")
|
// TODO fmt.Println(" keypairs verify --issuer https://example.com '{ \"sub\": \"xxxx\" }'")
|
||||||
fmt.Println("")
|
fmt.Println("")
|
||||||
}
|
}
|
||||||
|
|
||||||
func ver() {
|
func ver() string {
|
||||||
fmt.Printf("%s v%s %s (%s)\n", name, version, commit[:7], date)
|
return fmt.Sprintf("%s v%s (%s) %s", name, version, commit[:7], date)
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
args := os.Args[:]
|
args := os.Args[:]
|
||||||
|
|
||||||
if "help" == args[1] {
|
if len(args) < 2 || "help" == args[1] {
|
||||||
// top-level help
|
// top-level help
|
||||||
if 2 == len(args) {
|
if len(args) <= 2 {
|
||||||
usage()
|
usage()
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
return
|
return
|
||||||
@ -65,13 +70,17 @@ func main() {
|
|||||||
|
|
||||||
switch args[1] {
|
switch args[1] {
|
||||||
case "version":
|
case "version":
|
||||||
ver()
|
fmt.Println(ver())
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
return
|
return
|
||||||
case "gen":
|
case "gen":
|
||||||
gen(args[2:])
|
gen(args[2:])
|
||||||
case "sign":
|
case "sign":
|
||||||
sign(args[2:])
|
sign(args[2:])
|
||||||
|
case "decode":
|
||||||
|
fallthrough
|
||||||
|
case "inspect":
|
||||||
|
inspect(args[2:])
|
||||||
case "verify":
|
case "verify":
|
||||||
verify(args[2:])
|
verify(args[2:])
|
||||||
default:
|
default:
|
||||||
@ -83,15 +92,28 @@ func main() {
|
|||||||
|
|
||||||
func gen(args []string) {
|
func gen(args []string) {
|
||||||
var keyname string
|
var keyname string
|
||||||
|
var keynameAlt string
|
||||||
|
//var keynameAlt2 string
|
||||||
var pubname string
|
var pubname string
|
||||||
flags := flag.NewFlagSet("gen", flag.ExitOnError)
|
flags := flag.NewFlagSet("gen", flag.ExitOnError)
|
||||||
flags.StringVar(&keyname, "o", "", "private key file (ex: key.jwk.json or key.pem)")
|
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(&pubname, "pub", "", "public key file (ex: pub.jwk.json or pub.pem)")
|
flags.StringVar(&pubname, "pub", "", "public key file (ex: pub.jwk.json or pub.pem)")
|
||||||
flags.Parse(args)
|
flags.Parse(args)
|
||||||
|
|
||||||
|
if 0 == len(keyname) {
|
||||||
|
keyname = keynameAlt
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
if 0 == len(keyname) {
|
||||||
|
keyname = keynameAlt2
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
key := keypairs.NewDefaultPrivateKey()
|
key := keypairs.NewDefaultPrivateKey()
|
||||||
marshalPriv(key, keyname)
|
marshalPriv(key, keyname)
|
||||||
pub := keypairs.NewPublicKey(key.Public())
|
pub := key.Public().(keypairs.PublicKey)
|
||||||
marshalPub(pub, pubname)
|
marshalPub(pub, pubname)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,19 +184,109 @@ func sign(args []string) {
|
|||||||
fmt.Fprintf(os.Stdout, "%s\n", keypairs.JWSToJWT(jws))
|
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) {
|
func verify(args []string) {
|
||||||
flags := flag.NewFlagSet("verify", flag.ExitOnError)
|
flags := flag.NewFlagSet("verify", flag.ExitOnError)
|
||||||
flags.Usage = func() {
|
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("")
|
||||||
fmt.Println(" <public key>: a File or String of an EC or RSA key in JWK or PEM format")
|
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(" <jwt-or-jws>: a JWT or JWS File or String, if JWS the payload must be Base64")
|
||||||
fmt.Println("")
|
fmt.Println("")
|
||||||
}
|
}
|
||||||
flags.Parse(args)
|
flags.Parse(args)
|
||||||
if len(flags.Args()) <= 1 {
|
if len(flags.Args()) < 1 {
|
||||||
flags.Usage()
|
flags.Usage()
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if 1 == len(flags.Args()) {
|
||||||
|
inspect(args)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
pubname := flags.Args()[0]
|
pubname := flags.Args()[0]
|
||||||
@ -248,26 +360,26 @@ func readPub(pubname string) (keypairs.PublicKey, error) {
|
|||||||
b, err := ioutil.ReadFile(pubname)
|
b, err := ioutil.ReadFile(pubname)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
// No file? Try as string!
|
// No file? Try as string!
|
||||||
var err2 error
|
pub2, err2 := keypairs.ParsePublicKey([]byte(pubname))
|
||||||
pub, err2 = keypairs.ParsePublicKey([]byte(pubname))
|
|
||||||
if nil != err2 {
|
if nil != err2 {
|
||||||
return nil, fmt.Errorf(
|
return nil, fmt.Errorf(
|
||||||
"could not read public key as file (or parse as string) %q:\n%w",
|
"could not read public key as file (or parse as string) %q:\n%w",
|
||||||
pubname, err,
|
pubname, err,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
pub = pub2.Key()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Oh, it was a file.
|
// Oh, it was a file.
|
||||||
if nil == pub {
|
if nil == pub {
|
||||||
var err3 error
|
pub3, err3 := keypairs.ParsePublicKey(b)
|
||||||
pub, err3 = keypairs.ParsePublicKey(b)
|
|
||||||
if nil != err3 {
|
if nil != err3 {
|
||||||
return nil, fmt.Errorf(
|
return nil, fmt.Errorf(
|
||||||
"could not parse public key from file %q:\n%w",
|
"could not parse public key from file %q:\n%w",
|
||||||
pubname, err3,
|
pubname, err3,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
pub = pub3.Key()
|
||||||
}
|
}
|
||||||
|
|
||||||
return pub, nil
|
return pub, nil
|
||||||
|
|||||||
@ -25,8 +25,8 @@ import (
|
|||||||
|
|
||||||
// TODO should be ErrInvalidJWKURL
|
// TODO should be ErrInvalidJWKURL
|
||||||
|
|
||||||
// EInvalidJWKURL means that the url did not provide JWKs
|
// ErrInvalidJWKURL means that the url did not provide JWKs
|
||||||
var EInvalidJWKURL = errors.New("url does not lead to valid JWKs")
|
var ErrInvalidJWKURL = errors.New("url does not lead to valid JWKs")
|
||||||
|
|
||||||
// KeyCache is an in-memory key cache
|
// KeyCache is an in-memory key cache
|
||||||
var KeyCache = map[string]CachableKey{}
|
var KeyCache = map[string]CachableKey{}
|
||||||
@ -41,7 +41,7 @@ var ErrInsecureDomain = errors.New("Whitelists should only allow secure URLs (i.
|
|||||||
|
|
||||||
// CachableKey represents
|
// CachableKey represents
|
||||||
type CachableKey struct {
|
type CachableKey struct {
|
||||||
Key keypairs.PublicKey
|
Key keypairs.PublicKeyDeprecated
|
||||||
Expiry time.Time
|
Expiry time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,7 +80,7 @@ var MinimumKeyDuration = time.Hour
|
|||||||
var MaximumKeyDuration = 72 * time.Hour
|
var MaximumKeyDuration = 72 * time.Hour
|
||||||
|
|
||||||
// PublicKeysMap is a newtype for a map of keypairs.PublicKey
|
// PublicKeysMap is a newtype for a map of keypairs.PublicKey
|
||||||
type PublicKeysMap map[string]keypairs.PublicKey
|
type PublicKeysMap = map[string]keypairs.PublicKeyDeprecated
|
||||||
|
|
||||||
// OIDCJWKs fetches baseURL + ".well-known/openid-configuration" and then fetches and returns the Public Keys.
|
// 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) {
|
||||||
@ -135,20 +135,31 @@ func JWK(kidOrThumb, iss string) (keypairs.PublicKey, error) {
|
|||||||
// PEM tries to return a key from cache, falling back to the specified PEM url
|
// PEM tries to return a key from cache, falling back to the specified PEM url
|
||||||
func PEM(url string) (keypairs.PublicKey, error) {
|
func PEM(url string) (keypairs.PublicKey, error) {
|
||||||
// url is kid in this case
|
// url is kid in this case
|
||||||
return immediateOneOrFetch(url, url, func(string) (map[string]map[string]string, map[string]keypairs.PublicKey, error) {
|
return immediateOneOrFetch(url, url, func(string) (map[string]map[string]string, map[string]keypairs.PublicKeyDeprecated, error) {
|
||||||
m, key, err := uncached.PEM(url)
|
m, key, err := uncached.PEM(url)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
return nil, 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
|
// put in a map, just for caching
|
||||||
maps := map[string]map[string]string{}
|
maps := map[string]map[string]string{}
|
||||||
maps[key.Thumbprint()] = m
|
maps[keypairs.Thumbprint(key)] = m
|
||||||
maps[url] = m
|
maps[url] = m
|
||||||
|
|
||||||
keys := map[string]keypairs.PublicKey{}
|
keys := uncached.PublicKeysMap{} // map[string]keypairs.PublicKeyDeprecated{}
|
||||||
keys[key.Thumbprint()] = key
|
keys[keypairs.Thumbprint(key)] = pubd
|
||||||
keys[url] = key
|
keys[url] = pubd
|
||||||
|
|
||||||
return maps, keys, nil
|
return maps, keys, nil
|
||||||
})
|
})
|
||||||
@ -157,7 +168,8 @@ func PEM(url string) (keypairs.PublicKey, error) {
|
|||||||
// Fetch returns a key from cache, falling back to an exact url as the "issuer"
|
// Fetch returns a key from cache, falling back to an exact url as the "issuer"
|
||||||
func Fetch(url string) (keypairs.PublicKey, error) {
|
func Fetch(url string) (keypairs.PublicKey, error) {
|
||||||
// url is kid in this case
|
// url is kid in this case
|
||||||
return immediateOneOrFetch(url, url, func(string) (map[string]map[string]string, map[string]keypairs.PublicKey, error) {
|
return immediateOneOrFetch(url, url,
|
||||||
|
func(string) (map[string]map[string]string, map[string]keypairs.PublicKeyDeprecated, error) {
|
||||||
m, key, err := uncached.Fetch(url)
|
m, key, err := uncached.Fetch(url)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
@ -165,10 +177,10 @@ func Fetch(url string) (keypairs.PublicKey, error) {
|
|||||||
|
|
||||||
// put in a map, just for caching
|
// put in a map, just for caching
|
||||||
maps := map[string]map[string]string{}
|
maps := map[string]map[string]string{}
|
||||||
maps[key.Thumbprint()] = m
|
maps[keypairs.Thumbprint(key.Key())] = m
|
||||||
|
|
||||||
keys := map[string]keypairs.PublicKey{}
|
keys := map[string]keypairs.PublicKeyDeprecated{}
|
||||||
keys[key.Thumbprint()] = key
|
keys[keypairs.Thumbprint(key.Key())] = key
|
||||||
|
|
||||||
return maps, keys, nil
|
return maps, keys, nil
|
||||||
})
|
})
|
||||||
@ -178,7 +190,7 @@ func Fetch(url string) (keypairs.PublicKey, error) {
|
|||||||
// The issuer string may be empty if using a thumbprint rather than a kid.
|
// The issuer string may be empty if using a thumbprint rather than a kid.
|
||||||
func Get(kidOrThumb, iss string) keypairs.PublicKey {
|
func Get(kidOrThumb, iss string) keypairs.PublicKey {
|
||||||
if pub := get(kidOrThumb, iss); nil != pub {
|
if pub := get(kidOrThumb, iss); nil != pub {
|
||||||
return pub.Key
|
return pub.Key.Key()
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -212,21 +224,21 @@ func get(kidOrThumb, iss string) *CachableKey {
|
|||||||
|
|
||||||
func immediateOneOrFetch(kidOrThumb, iss string, fetcher myfetcher) (keypairs.PublicKey, error) {
|
func immediateOneOrFetch(kidOrThumb, iss string, fetcher myfetcher) (keypairs.PublicKey, error) {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
key := get(kidOrThumb, iss)
|
hit := get(kidOrThumb, iss)
|
||||||
|
|
||||||
if nil == key {
|
if nil == hit {
|
||||||
return fetchAndSelect(kidOrThumb, iss, fetcher)
|
return fetchAndSelect(kidOrThumb, iss, fetcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch just a little before the key actually expires
|
// Fetch just a little before the key actually expires
|
||||||
if key.Expiry.Sub(now) <= StaleTime {
|
if hit.Expiry.Sub(now) <= StaleTime {
|
||||||
go fetchAndSelect(kidOrThumb, iss, fetcher)
|
go fetchAndSelect(kidOrThumb, iss, fetcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
return key.Key, nil
|
return hit.Key.Key(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type myfetcher func(string) (map[string]map[string]string, map[string]keypairs.PublicKey, error)
|
type myfetcher func(string) (map[string]map[string]string, map[string]keypairs.PublicKeyDeprecated, error)
|
||||||
|
|
||||||
func fetchAndSelect(id, baseURL string, fetcher myfetcher) (keypairs.PublicKey, error) {
|
func fetchAndSelect(id, baseURL string, fetcher myfetcher) (keypairs.PublicKey, error) {
|
||||||
maps, keys, err := fetcher(baseURL)
|
maps, keys, err := fetcher(baseURL)
|
||||||
@ -237,20 +249,21 @@ func fetchAndSelect(id, baseURL string, fetcher myfetcher) (keypairs.PublicKey,
|
|||||||
|
|
||||||
for i := range keys {
|
for i := range keys {
|
||||||
key := keys[i]
|
key := keys[i]
|
||||||
|
pub := key.Key()
|
||||||
|
|
||||||
if id == key.Thumbprint() {
|
if id == keypairs.Thumbprint(pub) {
|
||||||
return key, nil
|
return pub, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if id == key.KeyID() {
|
if id == key.KeyID() {
|
||||||
return key, nil
|
return pub, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, fmt.Errorf("Key identified by '%s' was not found at %s", id, baseURL)
|
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) {
|
func cacheKeys(maps map[string]map[string]string, keys PublicKeysMap, issuer string) {
|
||||||
for i := range keys {
|
for i := range keys {
|
||||||
key := keys[i]
|
key := keys[i]
|
||||||
m := maps[i]
|
m := maps[i]
|
||||||
@ -260,10 +273,13 @@ func cacheKeys(maps map[string]map[string]string, keys map[string]keypairs.Publi
|
|||||||
}
|
}
|
||||||
iss = normalizeIssuer(iss)
|
iss = normalizeIssuer(iss)
|
||||||
cacheKey(m["kid"], iss, m["exp"], key)
|
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.PublicKey) error {
|
func cacheKey(kid, iss, expstr string, pub keypairs.PublicKeyDeprecated) error {
|
||||||
var expiry time.Time
|
var expiry time.Time
|
||||||
iss = normalizeIssuer(iss)
|
iss = normalizeIssuer(iss)
|
||||||
|
|
||||||
@ -287,7 +303,7 @@ func cacheKey(kid, iss, expstr string, pub keypairs.PublicKey) error {
|
|||||||
Expiry: expiry,
|
Expiry: expiry,
|
||||||
}
|
}
|
||||||
// Since thumbprints are crypto secure, iss isn't needed
|
// Since thumbprints are crypto secure, iss isn't needed
|
||||||
thumb := pub.Thumbprint()
|
thumb := keypairs.Thumbprint(pub.Key())
|
||||||
KeyCache[thumb] = CachableKey{
|
KeyCache[thumb] = CachableKey{
|
||||||
Key: pub,
|
Key: pub,
|
||||||
Expiry: expiry,
|
Expiry: expiry,
|
||||||
|
|||||||
@ -11,16 +11,19 @@ import (
|
|||||||
var pubkey keypairs.PublicKey
|
var pubkey keypairs.PublicKey
|
||||||
|
|
||||||
func TestCachesKey(t *testing.T) {
|
func TestCachesKey(t *testing.T) {
|
||||||
|
// TODO set KeyID() in cache
|
||||||
testCachesKey(t, "https://bigsquid.auth0.com/")
|
testCachesKey(t, "https://bigsquid.auth0.com/")
|
||||||
clear()
|
clear()
|
||||||
testCachesKey(t, "https://bigsquid.auth0.com")
|
testCachesKey(t, "https://bigsquid.auth0.com")
|
||||||
// Get PEM
|
// Get PEM
|
||||||
k3, err := PEM("https://bigsquid.auth0.com/pem")
|
pubk3, err := PEM("https://bigsquid.auth0.com/pem")
|
||||||
if nil != err {
|
if nil != err {
|
||||||
t.Fatal("Error fetching and caching key:", err)
|
t.Fatal("[0] Error fetching and caching key:", err)
|
||||||
}
|
}
|
||||||
if k3.Thumbprint() != pubkey.Thumbprint() {
|
thumb3 := keypairs.Thumbprint(pubk3)
|
||||||
t.Fatal("Error got different thumbprint for different versions of the same key:", err)
|
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)
|
||||||
}
|
}
|
||||||
clear()
|
clear()
|
||||||
testCachesKey(t, "https://big-squid.github.io/")
|
testCachesKey(t, "https://big-squid.github.io/")
|
||||||
@ -47,10 +50,10 @@ func testCachesKey(t *testing.T, url string) {
|
|||||||
|
|
||||||
var key keypairs.PublicKey
|
var key keypairs.PublicKey
|
||||||
for i := range keys {
|
for i := range keys {
|
||||||
key = keys[i]
|
key = keys[i].Key()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
thumb := key.Thumbprint()
|
thumb := keypairs.Thumbprint(key)
|
||||||
|
|
||||||
// Look in cache for each (and fail)
|
// Look in cache for each (and fail)
|
||||||
if pub := Get(thumb, ""); nil != pub {
|
if pub := Get(thumb, ""); nil != pub {
|
||||||
@ -60,20 +63,25 @@ func testCachesKey(t *testing.T, url string) {
|
|||||||
// Get with caching
|
// Get with caching
|
||||||
pubkey, err = OIDCJWK(thumb, url)
|
pubkey, err = OIDCJWK(thumb, url)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
t.Fatal("Error fetching and caching key:", err)
|
t.Fatal("[1] Error fetching and caching key:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Look in cache for each (and succeed)
|
// Look in cache for each (and succeed)
|
||||||
if pub := Get(thumb, ""); nil == pub {
|
if pub := Get(thumb, ""); nil == pub {
|
||||||
t.Fatal("key was not properly cached by thumbprint", thumb)
|
t.Fatal("key was not properly cached by thumbprint", thumb)
|
||||||
}
|
}
|
||||||
if "" != pubkey.KeyID() {
|
|
||||||
if pub := Get(pubkey.KeyID(), url); nil == pub {
|
// TODO thumb / id mapping
|
||||||
t.Fatal("key was not properly cached by kid", pubkey.KeyID())
|
thumb = keypairs.Thumbprint(pubkey)
|
||||||
|
if pub := Get(thumb, url); nil == pub {
|
||||||
|
t.Fatal("key was not properly cached by kid", pubkey)
|
||||||
}
|
}
|
||||||
} else {
|
// TODO
|
||||||
t.Log("Key did not have an explicit KeyID")
|
/*
|
||||||
|
if 0 == len(keyfetch.GetID(thumb)) {
|
||||||
|
t.Log("Key did not have an explicit KeyID", thumb)
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
// Get again (should be sub-ms instant)
|
// Get again (should be sub-ms instant)
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
@ -86,8 +94,10 @@ func testCachesKey(t *testing.T, url string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Sanity check that the kid and thumb match
|
// Sanity check that the kid and thumb match
|
||||||
if key.KeyID() != pubkey.KeyID() || key.Thumbprint() != pubkey.Thumbprint() {
|
if !key.Equal(pubkey) || keypairs.Thumbprint(key) != keypairs.Thumbprint(pubkey) {
|
||||||
t.Fatal("SANITY: KeyIDs or Thumbprints do not match:", key.KeyID(), pubkey.KeyID(), key.Thumbprint(), pubkey.Thumbprint())
|
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))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get 404
|
// Get 404
|
||||||
|
|||||||
@ -4,6 +4,8 @@ package uncached
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/ecdsa"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
@ -15,8 +17,17 @@ import (
|
|||||||
"git.rootprojects.org/root/keypairs"
|
"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
|
// 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) {
|
func OIDCJWKs(baseURL string) (JWKMapByID, PublicKeysMap, error) {
|
||||||
baseURL = normalizeBaseURL(baseURL)
|
baseURL = normalizeBaseURL(baseURL)
|
||||||
oidcConf := struct {
|
oidcConf := struct {
|
||||||
JWKSURI string `json:"jwks_uri"`
|
JWKSURI string `json:"jwks_uri"`
|
||||||
@ -37,7 +48,7 @@ func OIDCJWKs(baseURL string) (map[string]map[string]string, map[string]keypairs
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WellKnownJWKs calls JWKs with baseURL + /.well-known/jwks.json as constructs the jwks_uri
|
// 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) {
|
func WellKnownJWKs(baseURL string) (JWKMapByID, PublicKeysMap, error) {
|
||||||
baseURL = normalizeBaseURL(baseURL)
|
baseURL = normalizeBaseURL(baseURL)
|
||||||
url := baseURL + ".well-known/jwks.json"
|
url := baseURL + ".well-known/jwks.json"
|
||||||
|
|
||||||
@ -45,9 +56,9 @@ func WellKnownJWKs(baseURL string) (map[string]map[string]string, map[string]key
|
|||||||
}
|
}
|
||||||
|
|
||||||
// JWKs fetches and parses a jwks.json (assuming well-known format)
|
// 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) {
|
func JWKs(jwksurl string) (JWKMapByID, PublicKeysMap, error) {
|
||||||
keys := map[string]keypairs.PublicKey{}
|
keys := PublicKeysMap{}
|
||||||
maps := map[string]map[string]string{}
|
maps := JWKMapByID{}
|
||||||
resp := struct {
|
resp := struct {
|
||||||
Keys []map[string]interface{} `json:"keys"`
|
Keys []map[string]interface{} `json:"keys"`
|
||||||
}{
|
}{
|
||||||
@ -71,8 +82,8 @@ func JWKs(jwksurl string) (map[string]map[string]string, map[string]keypairs.Pub
|
|||||||
if nil != err {
|
if nil != err {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
keys[key.Thumbprint()] = key
|
keys[keypairs.Thumbprint(key.Key())] = key
|
||||||
maps[key.Thumbprint()] = m
|
maps[keypairs.Thumbprint(key.Key())] = m
|
||||||
}
|
}
|
||||||
|
|
||||||
return maps, keys, nil
|
return maps, keys, nil
|
||||||
@ -80,32 +91,38 @@ func JWKs(jwksurl string) (map[string]map[string]string, map[string]keypairs.Pub
|
|||||||
|
|
||||||
// PEM fetches and parses a PEM (assuming well-known format)
|
// PEM fetches and parses a PEM (assuming well-known format)
|
||||||
func PEM(pemurl string) (map[string]string, keypairs.PublicKey, error) {
|
func PEM(pemurl string) (map[string]string, keypairs.PublicKey, error) {
|
||||||
var pub keypairs.PublicKey
|
var pubd keypairs.PublicKeyDeprecated
|
||||||
if err := safeFetch(pemurl, func(body io.Reader) error {
|
if err := safeFetch(pemurl, func(body io.Reader) error {
|
||||||
pem, err := ioutil.ReadAll(body)
|
pem, err := ioutil.ReadAll(body)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
pub, err = keypairs.ParsePublicKey(pem)
|
pubd, err = keypairs.ParsePublicKey(pem)
|
||||||
|
if nil != err {
|
||||||
return err
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}); nil != err {
|
}); nil != err {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
jwk := map[string]interface{}{}
|
jwk := map[string]interface{}{}
|
||||||
|
pub := pubd.Key()
|
||||||
body := bytes.NewBuffer(keypairs.MarshalJWKPublicKey(pub))
|
body := bytes.NewBuffer(keypairs.MarshalJWKPublicKey(pub))
|
||||||
decoder := json.NewDecoder(body)
|
decoder := json.NewDecoder(body)
|
||||||
decoder.UseNumber()
|
decoder.UseNumber()
|
||||||
_ = decoder.Decode(&jwk)
|
_ = decoder.Decode(&jwk)
|
||||||
|
|
||||||
m := getStringMap(jwk)
|
m := getStringMap(jwk)
|
||||||
m["kid"] = pemurl
|
m["kid"] = keypairs.Thumbprint(pub)
|
||||||
|
// TODO is this just junk?
|
||||||
|
m[URLishKey] = pemurl
|
||||||
|
|
||||||
switch p := pub.(type) {
|
switch pub.(type) {
|
||||||
case *keypairs.ECPublicKey:
|
case *ecdsa.PublicKey:
|
||||||
p.KID = pemurl
|
//p.KID = pemurl
|
||||||
case *keypairs.RSAPublicKey:
|
case *rsa.PublicKey:
|
||||||
p.KID = pemurl
|
//p.KID = pemurl
|
||||||
default:
|
default:
|
||||||
return nil, nil, errors.New("impossible key type")
|
return nil, nil, errors.New("impossible key type")
|
||||||
}
|
}
|
||||||
@ -114,7 +131,7 @@ func PEM(pemurl string) (map[string]string, keypairs.PublicKey, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fetch retrieves a single JWK (plain, bare jwk) from a URL (off-spec)
|
// Fetch retrieves a single JWK (plain, bare jwk) from a URL (off-spec)
|
||||||
func Fetch(url string) (map[string]string, keypairs.PublicKey, error) {
|
func Fetch(url string) (map[string]string, keypairs.PublicKeyDeprecated, error) {
|
||||||
var m map[string]interface{}
|
var m map[string]interface{}
|
||||||
if err := safeFetch(url, func(body io.Reader) error {
|
if err := safeFetch(url, func(body io.Reader) error {
|
||||||
decoder := json.NewDecoder(body)
|
decoder := json.NewDecoder(body)
|
||||||
|
|||||||
74
keypairs.go
74
keypairs.go
@ -3,7 +3,6 @@ package keypairs
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto"
|
"crypto"
|
||||||
"crypto/dsa"
|
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
"crypto/elliptic"
|
"crypto/elliptic"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
@ -57,14 +56,21 @@ const ErrDevBadKeyType = "[Developer Error] crypto.PublicKey and crypto.PrivateK
|
|||||||
// PrivateKey is a zero-cost typesafe substitue for crypto.PrivateKey
|
// PrivateKey is a zero-cost typesafe substitue for crypto.PrivateKey
|
||||||
type PrivateKey interface {
|
type PrivateKey interface {
|
||||||
Public() crypto.PublicKey
|
Public() crypto.PublicKey
|
||||||
|
Equal(x crypto.PrivateKey) bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// PublicKey thinly veils crypto.PublicKey for type safety
|
// PublicKey is so that v0.7.x can use golang v1.15 keys
|
||||||
type PublicKey interface {
|
type PublicKey interface {
|
||||||
|
Equal(x crypto.PublicKey) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublicKeyDeprecated thinly veils crypto.PublicKey for type safety
|
||||||
|
type PublicKeyDeprecated interface {
|
||||||
crypto.PublicKey
|
crypto.PublicKey
|
||||||
Thumbprint() string
|
//Equal(x crypto.PublicKey) bool
|
||||||
|
//Thumbprint() string
|
||||||
KeyID() string
|
KeyID() string
|
||||||
Key() crypto.PublicKey
|
Key() PublicKey
|
||||||
ExpiresAt() time.Time
|
ExpiresAt() time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,13 +93,18 @@ func (p *ECPublicKey) Thumbprint() string {
|
|||||||
return ThumbprintUntypedPublicKey(p.PublicKey)
|
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
|
// 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
|
// Key returns the PublicKey
|
||||||
func (p *ECPublicKey) Key() crypto.PublicKey {
|
func (p *ECPublicKey) Key() PublicKey {
|
||||||
return p.PublicKey
|
return p.PublicKey
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,13 +123,18 @@ func (p *RSAPublicKey) Thumbprint() string {
|
|||||||
return ThumbprintUntypedPublicKey(p.PublicKey)
|
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
|
// 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
|
// Key returns the PublicKey
|
||||||
func (p *RSAPublicKey) Key() crypto.PublicKey {
|
func (p *RSAPublicKey) Key() PublicKey {
|
||||||
return p.PublicKey
|
return p.PublicKey
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -133,8 +149,13 @@ func (p *RSAPublicKey) ExpiresAt() time.Time {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewPublicKey wraps a crypto.PublicKey to make it typesafe.
|
// NewPublicKey wraps a crypto.PublicKey to make it typesafe.
|
||||||
func NewPublicKey(pub crypto.PublicKey, kid ...string) PublicKey {
|
func NewPublicKey(pub crypto.PublicKey, kid ...string) PublicKeyDeprecated {
|
||||||
var k PublicKey
|
_, ok := pub.(PublicKey)
|
||||||
|
if !ok {
|
||||||
|
panic("Developer Error: not a crypto.PublicKey")
|
||||||
|
}
|
||||||
|
|
||||||
|
var k PublicKeyDeprecated
|
||||||
switch p := pub.(type) {
|
switch p := pub.(type) {
|
||||||
case *ecdsa.PublicKey:
|
case *ecdsa.PublicKey:
|
||||||
eckey := &ECPublicKey{
|
eckey := &ECPublicKey{
|
||||||
@ -156,14 +177,6 @@ func NewPublicKey(pub crypto.PublicKey, kid ...string) PublicKey {
|
|||||||
rsakey.KID = ThumbprintRSAPublicKey(p)
|
rsakey.KID = ThumbprintRSAPublicKey(p)
|
||||||
}
|
}
|
||||||
k = rsakey
|
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:
|
default:
|
||||||
panic(fmt.Errorf(ErrDevBadKeyType, pub))
|
panic(fmt.Errorf(ErrDevBadKeyType, pub))
|
||||||
}
|
}
|
||||||
@ -175,13 +188,11 @@ func NewPublicKey(pub crypto.PublicKey, kid ...string) PublicKey {
|
|||||||
// making it suitable for use as an OIDC public key.
|
// making it suitable for use as an OIDC public key.
|
||||||
func MarshalJWKPublicKey(key PublicKey, exp ...time.Time) []byte {
|
func MarshalJWKPublicKey(key PublicKey, exp ...time.Time) []byte {
|
||||||
// thumbprint keys are alphabetically sorted and only include the necessary public parts
|
// thumbprint keys are alphabetically sorted and only include the necessary public parts
|
||||||
switch k := key.Key().(type) {
|
switch k := key.(type) {
|
||||||
case *rsa.PublicKey:
|
case *rsa.PublicKey:
|
||||||
return MarshalRSAPublicKey(k, exp...)
|
return MarshalRSAPublicKey(k, exp...)
|
||||||
case *ecdsa.PublicKey:
|
case *ecdsa.PublicKey:
|
||||||
return MarshalECPublicKey(k, exp...)
|
return MarshalECPublicKey(k, exp...)
|
||||||
case *dsa.PublicKey:
|
|
||||||
panic(ErrInvalidPublicKey)
|
|
||||||
default:
|
default:
|
||||||
// this is unreachable because we know the types that we pass in
|
// this is unreachable because we know the types that we pass in
|
||||||
log.Printf("keytype: %t, %+v\n", key, key)
|
log.Printf("keytype: %t, %+v\n", key, key)
|
||||||
@ -189,8 +200,13 @@ 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
|
// ThumbprintPublicKey returns the SHA256 RFC-spec JWK thumbprint
|
||||||
func ThumbprintPublicKey(pub PublicKey) string {
|
func ThumbprintPublicKey(pub PublicKeyDeprecated) string {
|
||||||
return ThumbprintUntypedPublicKey(pub.Key())
|
return ThumbprintUntypedPublicKey(pub.Key())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -198,7 +214,7 @@ func ThumbprintPublicKey(pub PublicKey) string {
|
|||||||
// (but will still panic, to help you discover bugs in development rather than production).
|
// (but will still panic, to help you discover bugs in development rather than production).
|
||||||
func ThumbprintUntypedPublicKey(pub crypto.PublicKey) string {
|
func ThumbprintUntypedPublicKey(pub crypto.PublicKey) string {
|
||||||
switch p := pub.(type) {
|
switch p := pub.(type) {
|
||||||
case PublicKey:
|
case PublicKeyDeprecated:
|
||||||
return ThumbprintUntypedPublicKey(p.Key())
|
return ThumbprintUntypedPublicKey(p.Key())
|
||||||
case *ecdsa.PublicKey:
|
case *ecdsa.PublicKey:
|
||||||
return ThumbprintECPublicKey(p)
|
return ThumbprintECPublicKey(p)
|
||||||
@ -363,7 +379,7 @@ func getPEMBytes(block []byte) ([][]byte, error) {
|
|||||||
|
|
||||||
// ParsePublicKey will try to parse the bytes you give it
|
// 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
|
// in any of the supported formats: PEM, DER, PKIX/SPKI, PKCS1, x509 Certificate, and JWK
|
||||||
func ParsePublicKey(block []byte) (PublicKey, error) {
|
func ParsePublicKey(block []byte) (PublicKeyDeprecated, error) {
|
||||||
blocks, err := getPEMBytes(block)
|
blocks, err := getPEMBytes(block)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
return nil, ErrParsePublicKey
|
return nil, ErrParsePublicKey
|
||||||
@ -390,11 +406,11 @@ func ParsePublicKey(block []byte) (PublicKey, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ParsePublicKeyString calls ParsePublicKey([]byte(key)) for all you lazy folk.
|
// ParsePublicKeyString calls ParsePublicKey([]byte(key)) for all you lazy folk.
|
||||||
func ParsePublicKeyString(block string) (PublicKey, error) {
|
func ParsePublicKeyString(block string) (PublicKeyDeprecated, error) {
|
||||||
return ParsePublicKey([]byte(block))
|
return ParsePublicKey([]byte(block))
|
||||||
}
|
}
|
||||||
|
|
||||||
func parsePublicKey(der []byte) (PublicKey, error) {
|
func parsePublicKey(der []byte) (PublicKeyDeprecated, error) {
|
||||||
cert, err := x509.ParseCertificate(der)
|
cert, err := x509.ParseCertificate(der)
|
||||||
if nil == err {
|
if nil == err {
|
||||||
switch k := cert.PublicKey.(type) {
|
switch k := cert.PublicKey.(type) {
|
||||||
@ -440,7 +456,7 @@ func parsePublicKey(der []byte) (PublicKey, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewJWKPublicKey contstructs a PublicKey from the relevant pieces a map[string]string (generic JSON)
|
// NewJWKPublicKey contstructs a PublicKey from the relevant pieces a map[string]string (generic JSON)
|
||||||
func NewJWKPublicKey(m map[string]string) (PublicKey, error) {
|
func NewJWKPublicKey(m map[string]string) (PublicKeyDeprecated, error) {
|
||||||
switch m["kty"] {
|
switch m["kty"] {
|
||||||
case "RSA":
|
case "RSA":
|
||||||
return parseRSAPublicKey(m)
|
return parseRSAPublicKey(m)
|
||||||
@ -452,7 +468,7 @@ func NewJWKPublicKey(m map[string]string) (PublicKey, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ParseJWKPublicKey parses a JSON-encoded JWK and returns a PublicKey, or a (hopefully) helpful error message
|
// ParseJWKPublicKey parses a JSON-encoded JWK and returns a PublicKey, or a (hopefully) helpful error message
|
||||||
func ParseJWKPublicKey(b []byte) (PublicKey, error) {
|
func ParseJWKPublicKey(b []byte) (PublicKeyDeprecated, error) {
|
||||||
// RSA and EC have "d" as a private part
|
// RSA and EC have "d" as a private part
|
||||||
if bytes.Contains(b, []byte(`"d"`)) {
|
if bytes.Contains(b, []byte(`"d"`)) {
|
||||||
return nil, ErrUnexpectedPrivateKey
|
return nil, ErrUnexpectedPrivateKey
|
||||||
@ -461,7 +477,7 @@ func ParseJWKPublicKey(b []byte) (PublicKey, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ParseJWKPublicKeyString calls ParseJWKPublicKey([]byte(key)) for all you lazy folk.
|
// ParseJWKPublicKeyString calls ParseJWKPublicKey([]byte(key)) for all you lazy folk.
|
||||||
func ParseJWKPublicKeyString(s string) (PublicKey, error) {
|
func ParseJWKPublicKeyString(s string) (PublicKeyDeprecated, error) {
|
||||||
if strings.Contains(s, `"d"`) {
|
if strings.Contains(s, `"d"`) {
|
||||||
return nil, ErrUnexpectedPrivateKey
|
return nil, ErrUnexpectedPrivateKey
|
||||||
}
|
}
|
||||||
@ -469,7 +485,7 @@ func ParseJWKPublicKeyString(s string) (PublicKey, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DecodeJWKPublicKey stream-decodes a JSON-encoded JWK and returns a PublicKey, or a (hopefully) helpful error message
|
// DecodeJWKPublicKey stream-decodes a JSON-encoded JWK and returns a PublicKey, or a (hopefully) helpful error message
|
||||||
func DecodeJWKPublicKey(r io.Reader) (PublicKey, error) {
|
func DecodeJWKPublicKey(r io.Reader) (PublicKeyDeprecated, error) {
|
||||||
m := make(map[string]string)
|
m := make(map[string]string)
|
||||||
if err := json.NewDecoder(r).Decode(&m); nil != err {
|
if err := json.NewDecoder(r).Decode(&m); nil != err {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -481,7 +497,7 @@ func DecodeJWKPublicKey(r io.Reader) (PublicKey, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// the underpinnings of the parser as used by the typesafe wrappers
|
// the underpinnings of the parser as used by the typesafe wrappers
|
||||||
func newJWKPublicKey(data interface{}) (PublicKey, error) {
|
func newJWKPublicKey(data interface{}) (PublicKeyDeprecated, error) {
|
||||||
var m map[string]string
|
var m map[string]string
|
||||||
|
|
||||||
switch d := data.(type) {
|
switch d := data.(type) {
|
||||||
|
|||||||
@ -39,7 +39,7 @@ var never = time.Time{}
|
|||||||
// Middleware holds your public keys and has http handler methods for OIDC and Auth0 JWKs
|
// Middleware holds your public keys and has http handler methods for OIDC and Auth0 JWKs
|
||||||
type Middleware struct {
|
type Middleware struct {
|
||||||
BaseURL *url.URL
|
BaseURL *url.URL
|
||||||
Keys []keypairs.PublicKey
|
Keys []keypairs.PublicKeyDeprecated
|
||||||
ExpiresIn time.Duration
|
ExpiresIn time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -148,7 +148,7 @@ func (m *Middleware) Auth0PEM(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func marshalJWKs(keys []keypairs.PublicKey, exp2 time.Time) []string {
|
func marshalJWKs(keys []keypairs.PublicKeyDeprecated, exp2 time.Time) []string {
|
||||||
jwks := make([]string, 0, 1)
|
jwks := make([]string, 0, 1)
|
||||||
|
|
||||||
for i := range keys {
|
for i := range keys {
|
||||||
@ -163,7 +163,8 @@ func marshalJWKs(keys []keypairs.PublicKey, exp2 time.Time) []string {
|
|||||||
|
|
||||||
// Note that you don't have to embed `iss` in the JWK because the client
|
// 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.
|
// already has that info by virtue of getting to it in the first place.
|
||||||
jwk := string(keypairs.MarshalJWKPublicKey(key, exp))
|
pub := key.Key()
|
||||||
|
jwk := string(keypairs.MarshalJWKPublicKey(pub, exp))
|
||||||
jwks = append(jwks, jwk)
|
jwks = append(jwks, jwk)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -18,7 +18,7 @@ import (
|
|||||||
|
|
||||||
func TestServeKeys(t *testing.T) {
|
func TestServeKeys(t *testing.T) {
|
||||||
eckey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
eckey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
pubs := []keypairs.PublicKey{
|
pubs := []keypairs.PublicKeyDeprecated{
|
||||||
keypairs.NewPublicKey(eckey.Public()),
|
keypairs.NewPublicKey(eckey.Public()),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,8 +42,9 @@ func TestServeKeys(t *testing.T) {
|
|||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
time.Sleep(15 * time.Second)
|
time.Sleep(15 * time.Second)
|
||||||
ctx, _ := context.WithTimeout(context.Background(), 15*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||||
h.Shutdown(ctx)
|
h.Shutdown(ctx)
|
||||||
|
cancel()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
m := map[string]string{}
|
m := map[string]string{}
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
package keypairs
|
package keypairs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto"
|
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
@ -14,7 +13,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// MarshalPEMPublicKey outputs the given public key as JWK
|
// MarshalPEMPublicKey outputs the given public key as JWK
|
||||||
func MarshalPEMPublicKey(pubkey crypto.PublicKey) ([]byte, error) {
|
func MarshalPEMPublicKey(pubkey PublicKey) ([]byte, error) {
|
||||||
block, err := marshalDERPublicKey(pubkey)
|
block, err := marshalDERPublicKey(pubkey)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -23,7 +22,7 @@ func MarshalPEMPublicKey(pubkey crypto.PublicKey) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MarshalDERPublicKey outputs the given public key as JWK
|
// MarshalDERPublicKey outputs the given public key as JWK
|
||||||
func MarshalDERPublicKey(pubkey crypto.PublicKey) ([]byte, error) {
|
func MarshalDERPublicKey(pubkey PublicKey) ([]byte, error) {
|
||||||
block, err := marshalDERPublicKey(pubkey)
|
block, err := marshalDERPublicKey(pubkey)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -32,7 +31,7 @@ func MarshalDERPublicKey(pubkey crypto.PublicKey) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// marshalDERPublicKey outputs the given public key as JWK
|
// marshalDERPublicKey outputs the given public key as JWK
|
||||||
func marshalDERPublicKey(pubkey crypto.PublicKey) (*pem.Block, error) {
|
func marshalDERPublicKey(pubkey PublicKey) (*pem.Block, error) {
|
||||||
|
|
||||||
var der []byte
|
var der []byte
|
||||||
var typ string
|
var typ string
|
||||||
|
|||||||
6
sign.go
6
sign.go
@ -26,7 +26,7 @@ func SignClaims(privkey PrivateKey, header Object, claims Object) (*JWS, error)
|
|||||||
//delete(header, "_seed")
|
//delete(header, "_seed")
|
||||||
}
|
}
|
||||||
|
|
||||||
protected, header, err := headerToProtected(NewPublicKey(privkey.Public()), header)
|
protected, header, err := headerToProtected(privkey.Public().(PublicKey), header)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
return 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.
|
// because that's all that's practical and well-supported.
|
||||||
// No security theatre here.
|
// No security theatre here.
|
||||||
alg := "ES256"
|
alg := "ES256"
|
||||||
switch pub.Key().(type) {
|
switch pub.(type) {
|
||||||
case *rsa.PublicKey:
|
case *rsa.PublicKey:
|
||||||
alg = "RS256"
|
alg = "RS256"
|
||||||
}
|
}
|
||||||
@ -80,7 +80,7 @@ func headerToProtected(pub PublicKey, header Object) ([]byte, Object, error) {
|
|||||||
// TODO what are the acceptable values? JWT. JWS? others?
|
// TODO what are the acceptable values? JWT. JWS? others?
|
||||||
header["typ"] = "JWT"
|
header["typ"] = "JWT"
|
||||||
if _, ok := header["jwk"]; !ok {
|
if _, ok := header["jwk"]; !ok {
|
||||||
thumbprint := ThumbprintPublicKey(pub)
|
thumbprint := ThumbprintPublicKey(NewPublicKey(pub))
|
||||||
kid, _ := header["kid"].(string)
|
kid, _ := header["kid"].(string)
|
||||||
if "" != kid && thumbprint != kid {
|
if "" != kid && thumbprint != kid {
|
||||||
return nil, nil, errors.New("'kid' should be the key's thumbprint")
|
return nil, nil, errors.New("'kid' should be the key's thumbprint")
|
||||||
|
|||||||
10
verify.go
10
verify.go
@ -73,7 +73,7 @@ func VerifyClaims(pubkey PublicKey, jws *JWS) (errs []error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func selfsignCheck(jwkmap Object, errs []error) (PublicKey, []error) {
|
func selfsignCheck(jwkmap Object, errs []error) (PublicKey, []error) {
|
||||||
var pub PublicKey = nil
|
var pub PublicKeyDeprecated = nil
|
||||||
log.Println("Security TODO: did not check jws.Claims[\"sub\"] against 'jwk'")
|
log.Println("Security TODO: did not check jws.Claims[\"sub\"] against 'jwk'")
|
||||||
log.Println("Security TODO: did not check jws.Claims[\"iss\"]")
|
log.Println("Security TODO: did not check jws.Claims[\"iss\"]")
|
||||||
kty := jwkmap["kty"]
|
kty := jwkmap["kty"]
|
||||||
@ -104,7 +104,7 @@ func selfsignCheck(jwkmap Object, errs []error) (PublicKey, []error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return pub, errs
|
return pub.Key(), errs
|
||||||
}
|
}
|
||||||
|
|
||||||
func pubkeyCheck(pubkey PublicKey, kid string, opts *keyOptions, errs []error) (PublicKey, []error) {
|
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
|
return nil, errs
|
||||||
}
|
}
|
||||||
privkey := newPrivateKey(opts)
|
privkey := newPrivateKey(opts)
|
||||||
pub = NewPublicKey(privkey.Public())
|
pub = privkey.Public().(PublicKey)
|
||||||
return pub, errs
|
return pub, errs
|
||||||
}
|
}
|
||||||
err := errors.New("no matching public key")
|
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 nil != pub && "" != kid {
|
||||||
if 1 != subtle.ConstantTimeCompare([]byte(kid), []byte(pub.Thumbprint())) {
|
if 1 != subtle.ConstantTimeCompare([]byte(kid), []byte(Thumbprint(pub))) {
|
||||||
err := errors.New("'kid' does not match the public key thumbprint")
|
err := errors.New("'kid' does not match the public key thumbprint")
|
||||||
errs = append(errs, err)
|
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
|
// Verify will check the signature of a hash
|
||||||
func Verify(pubkey PublicKey, hash []byte, sig []byte) bool {
|
func Verify(pubkey PublicKey, hash []byte, sig []byte) bool {
|
||||||
|
|
||||||
switch pub := pubkey.Key().(type) {
|
switch pub := pubkey.(type) {
|
||||||
case *rsa.PublicKey:
|
case *rsa.PublicKey:
|
||||||
//log.Printf("RSA VERIFY")
|
//log.Printf("RSA VERIFY")
|
||||||
// TODO Size(key) to detect key size ?
|
// TODO Size(key) to detect key size ?
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user