2
0
mirror of https://github.com/therootcompany/keypairs synced 2025-04-21 23:10:44 +00:00

Compare commits

..

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

18 changed files with 161 additions and 1216 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 +0,0 @@
AJ ONeal <aj@therootcompany.com> (https://therootcompany.com)

View File

@ -1,43 +1,4 @@
# [keypairs](https://git.rootprojects.org/root/keypairs) # go-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`
@ -53,7 +14,7 @@ jwk, err := keypairs.MarshalJWKPublicKey(pub, time.Now().Add(2 * time.Day))
kid, err := keypairs.ThumbprintPublicKey(pub) kid, err := keypairs.ThumbprintPublicKey(pub)
``` ```
# GoDoc API Documentation # API Documentation
See <https://pkg.go.dev/git.rootprojects.org/root/keypairs> See <https://pkg.go.dev/git.rootprojects.org/root/keypairs>
@ -95,8 +56,8 @@ between the ASN.1, x509, PEM, and JWK formats.
# LICENSE # LICENSE
Copyright (c) 2020-present AJ ONeal \ Copyright (c) 2020-present AJ ONeal
Copyright (c) 2018-2019 Big Squid, Inc. Copyright (c) 2018-2019 Big Squid, Inc.
This work is licensed under the terms of the MIT license. \ This work is licensed under the terms of the MIT license.
For a copy, see <https://opensource.org/licenses/MIT>. For a copy, see <https://opensource.org/licenses/MIT>.

View File

@ -1,19 +0,0 @@
#!/bin/bash
set -u
go build -mod=vendor cmd/keypairs/*.go
./keypairs gen > testkey.jwk.json 2> testpub.jwk.json
./keypairs sign --exp 1h ./testkey.jwk.json '{"foo":"bar"}' > testjwt.txt 2> testjws.json
echo ""
echo "Should pass:"
./keypairs verify ./testpub.jwk.json testjwt.txt > /dev/null
./keypairs verify ./testpub.jwk.json "$(cat testjwt.txt)" > /dev/null
./keypairs verify ./testpub.jwk.json testjws.json > /dev/null
./keypairs verify ./testpub.jwk.json "$(cat testjws.json)" > /dev/null
echo ""
echo "Should fail:"
./keypairs sign --exp -1m ./testkey.jwk.json '{"bar":"foo"}' > errjwt.txt 2> errjws.json
./keypairs verify ./testpub.jwk.json errjwt.txt > /dev/null

View File

@ -7,446 +7,54 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"strings" "strings"
"time"
"git.rootprojects.org/root/keypairs" "git.rootprojects.org/root/keypairs"
"git.rootprojects.org/root/keypairs/keyfetch"
) )
var (
name = "keypairs"
version = "0.0.0"
date = "0001-01-01T00:00:00Z"
commit = "0000000"
)
func usage() {
fmt.Println(ver())
fmt.Println()
fmt.Println("Usage")
fmt.Printf(" %s <command> [flags] args...\n", name)
fmt.Println("")
fmt.Printf("See usage: %s help <command>\n", name)
fmt.Println("")
fmt.Println("Commands:")
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("")
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 main() { func main() {
args := os.Args[:] if 1 == len(os.Args) || "gen" != os.Args[1] {
fmt.Fprintln(os.Stderr, "Usage: keypairs gen -o <filename> [--pub <filename>]")
if len(args) < 2 || "help" == args[1] {
// top-level help
if len(args) <= 2 {
usage()
os.Exit(0)
return
}
// move help to subcommand argument
self := args[0]
args = append([]string{self}, args[2:]...)
args = append(args, "--help")
}
switch args[1] {
case "version":
fmt.Println(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:
usage()
os.Exit(1) os.Exit(1)
return return
} }
}
func gen(args []string) { // gen subcommand
var keyname string args := os.Args[2:]
var keynameAlt string
//var keynameAlt2 string var privname string
var pubname string var pubname string
flags := flag.NewFlagSet("gen", flag.ExitOnError) flags := flag.NewFlagSet("gen", flag.ExitOnError)
flags.StringVar(&keynameAlt, "o", "", "output file (alias of --key)") flags.StringVar(&privname, "o", "", "private key file (should have .jwk.json or pkcs8.pem extension)")
//flags.StringVar(&keynameAlt2, "priv", "", "private key file (alias of --key)") flags.StringVar(&pubname, "pub", "", "public key file (should have .jwk.json or spki.pem extension)")
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.Parse(args) flags.Parse(args)
if 0 == len(keyname) { priv := keypairs.NewDefaultPrivateKey()
keyname = keynameAlt marshalPriv(priv, privname)
} marshalPub(keypairs.NewPublicKey(priv.Public()), pubname)
/*
if 0 == len(keyname) {
keyname = keynameAlt2
}
*/
key := keypairs.NewDefaultPrivateKey()
marshalPriv(key, keyname)
pub := key.Public().(keypairs.PublicKey)
marshalPub(pub, pubname)
} }
func sign(args []string) { func marshalPriv(priv keypairs.PrivateKey, privname string) {
var exp time.Duration if "" == privname {
flags := flag.NewFlagSet("sign", flag.ExitOnError) b := indentJSON(keypairs.MarshalJWKPrivateKey(priv))
flags.DurationVar(&exp, "exp", 0, "duration until token expires (Default 15m)")
flags.Parse(args)
if len(flags.Args()) <= 1 {
fmt.Fprintf(os.Stderr, "Usage: keypairs sign --exp 1h <private PEM or JWK> ./payload.json\n")
os.Exit(1)
}
keyname := flags.Args()[0]
payload := flags.Args()[1]
key, err := readKey(keyname)
if nil != err {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
return
}
if "" == payload {
// TODO should this be null? I forget
payload = "{}"
}
b, err := ioutil.ReadFile(payload)
claims := map[string]interface{}{}
if nil != err {
var err2 error
err2 = json.Unmarshal([]byte(payload), &claims)
if nil != err2 {
fmt.Fprintf(os.Stderr,
"could not read payload as file (or parse as string) %q: %s\n", payload, err)
os.Exit(1)
return
}
}
if 0 == len(claims) {
var err3 error
err3 = json.Unmarshal(b, &claims)
if nil != err3 {
fmt.Fprintf(os.Stderr,
"could not parse palyoad from file %q: %s\n", payload, err3)
os.Exit(1)
return
}
}
if 0 != exp {
claims["exp"] = exp.Seconds()
}
if _, ok := claims["exp"]; !ok {
claims["exp"] = (15 * time.Minute).Seconds()
}
jws, err := keypairs.SignClaims(key, nil, claims)
if nil != err {
fmt.Fprintf(os.Stderr, "could not sign claims: %v\n%#v\n", err, claims)
os.Exit(1)
return
}
b, _ = json.Marshal(&jws)
fmt.Fprintf(os.Stderr, "%s\n", indentJSON(b))
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("")
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 {
flags.Usage()
os.Exit(1)
return
}
if 1 == len(flags.Args()) {
inspect(args)
return
}
pubname := flags.Args()[0]
payload := flags.Args()[1]
pub, err := readPub(pubname)
if nil != err {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
return
}
jws, err := readJWS(payload)
if nil != err {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
return
}
b, _ := json.Marshal(&jws)
fmt.Fprintf(os.Stdout, "%s\n", indentJSON(b))
errs := keypairs.VerifyClaims(pub, jws)
if nil != errs {
fmt.Fprintf(os.Stderr, "error:\n")
for _, err := range errs {
fmt.Fprintf(os.Stderr, "\t%v\n", err)
}
os.Exit(1)
return
}
fmt.Fprintf(os.Stderr, "Signature is Valid\n")
}
func readKey(keyname string) (keypairs.PrivateKey, error) {
var key keypairs.PrivateKey = nil
// Read as file
b, err := ioutil.ReadFile(keyname)
if nil != err {
// Tis not a file! Perhaps a string?
var err2 error
key, err2 = keypairs.ParsePrivateKey([]byte(keyname))
if nil != err2 {
// Neither a valid string. Blast!
return nil, fmt.Errorf(
"could not read private key as file (or parse as string) %q:\n%s",
keyname, err2,
)
}
}
if nil == key {
var err3 error
key, err3 = keypairs.ParsePrivateKey(b)
if nil != err3 {
return nil, fmt.Errorf(
"could not parse private key from file %q:\n%s",
keyname, err3,
)
}
}
return key, nil
}
func readPub(pubname string) (keypairs.PublicKey, error) {
var pub keypairs.PublicKey = nil
// Read as file
b, err := ioutil.ReadFile(pubname)
if nil != err {
// No file? Try as string!
pub2, 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)
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
}
func readJWS(payload string) (*keypairs.JWS, error) {
// Is it a file?
b, err := ioutil.ReadFile(payload)
if nil != err {
// Or a JWS or JWS String!?
b = []byte(payload)
}
// Either way, we have some bytes now
jws := &keypairs.JWS{}
jwt := string(b)
jwsb := []byte(jwt)
if !strings.Contains(jwt, " \t\n{}[]") {
jws = keypairs.JWTToJWS(string(b))
if nil != jws {
b, _ = json.Marshal(jws)
jwsb = (b)
}
}
// And now we have a string that may be a JWS
if err := json.Unmarshal(jwsb, &jws); nil != err {
// Nope, it's not
return nil, fmt.Errorf(
"could not read signed payload from file or string as JWT or JWS %q:\n%w",
payload, err,
)
}
if err := jws.DecodeComponents(); nil != err {
// bah! so close!
return nil, fmt.Errorf(
"could not decode the JWS Header and Claims components: %w\n%s",
err, string(jwsb),
)
}
return jws, nil
}
func marshalPriv(key keypairs.PrivateKey, keyname string) {
if "" == keyname {
b := indentJSON(keypairs.MarshalJWKPrivateKey(key))
fmt.Fprintf(os.Stdout, string(b)+"\n") fmt.Fprintf(os.Stdout, string(b)+"\n")
return return
} }
var b []byte var b []byte
if strings.HasSuffix(keyname, ".json") { if strings.HasSuffix(privname, ".json") {
b = indentJSON(keypairs.MarshalJWKPrivateKey(key)) b = indentJSON(keypairs.MarshalJWKPrivateKey(priv))
} else if strings.HasSuffix(keyname, ".pem") { } else if strings.HasSuffix(privname, ".pem") {
b, _ = keypairs.MarshalPEMPrivateKey(key) b, _ = keypairs.MarshalPEMPrivateKey(priv)
} else if strings.HasSuffix(keyname, ".der") { } else if strings.HasSuffix(privname, ".der") {
b, _ = keypairs.MarshalDERPrivateKey(key) b, _ = keypairs.MarshalDERPrivateKey(priv)
} else { } else {
fmt.Fprintf(os.Stderr, "private key extension should be .jwk.json, .pem, or .der") fmt.Fprintf(os.Stderr, "private key extension should be .jwk.json, .pem, or .der")
os.Exit(1) os.Exit(1)
return return
} }
ioutil.WriteFile(keyname, b, 0600) ioutil.WriteFile(privname, b, 0600)
} }
func marshalPub(pub keypairs.PublicKey, pubname string) { func marshalPub(pub keypairs.PublicKey, pubname string) {
@ -473,5 +81,5 @@ func indentJSON(b []byte) []byte {
m := map[string]interface{}{} m := map[string]interface{}{}
_ = json.Unmarshal(b, &m) _ = json.Unmarshal(b, &m)
b, _ = json.MarshalIndent(&m, "", " ") b, _ = json.MarshalIndent(&m, "", " ")
return append(b, '\n') return b
} }

View File

@ -11,36 +11,34 @@ import (
) )
var randReader io.Reader = rand.Reader var randReader io.Reader = rand.Reader
var allowMocking = false var maxRetry = 1
// KeyOptions are the things that we may need to know about a request to fulfill it properly // KeyOptions are the things that we may need to know about a request to fulfill it properly
type keyOptions struct { type keyOptions struct {
//Key string `json:"key"` //Key string `json:"key"`
KeyType string `json:"kty"` KeyType string `json:"kty"`
mockSeed int64 //`json:"-"` //Seed int64 `json:"-"`
//SeedStr string `json:"seed"` //SeedStr string `json:"seed"`
//Claims Object `json:"claims"` //Claims Object `json:"claims"`
//Header Object `json:"header"` //Header Object `json:"header"`
} }
func (o *keyOptions) nextReader() io.Reader { // this shananigans is only for testing and debug API stuff
if allowMocking { func (o *keyOptions) myFooNextReader() io.Reader {
return o.maybeMockReader()
}
return randReader return randReader
/*
if 0 == o.Seed {
return randReader
}
return mathrand.New(mathrand.NewSource(o.Seed))
*/
} }
// NewDefaultPrivateKey generates a key with reasonable strength. // NewDefaultPrivateKey generates a key with reasonable strength.
// Today that means a 256-bit equivalent - either RSA 2048 or EC P-256. // Today that means a 256-bit equivalent - either RSA 2048 or EC P-256.
func NewDefaultPrivateKey() PrivateKey { func NewDefaultPrivateKey() PrivateKey {
// insecure random is okay here,
// it's just used for a coin toss
mathrand.Seed(time.Now().UnixNano()) mathrand.Seed(time.Now().UnixNano())
coin := mathrand.Int() coin := mathrand.Int()
// the idea here is that we want to make
// it dead simple to support RSA and EC
// so it shouldn't matter which is used
if 0 == coin%2 { if 0 == coin%2 {
return newPrivateKey(&keyOptions{ return newPrivateKey(&keyOptions{
KeyType: "RSA", KeyType: "RSA",
@ -57,13 +55,29 @@ func newPrivateKey(opts *keyOptions) PrivateKey {
if "RSA" == opts.KeyType { if "RSA" == opts.KeyType {
keylen := 2048 keylen := 2048
privkey, _ = rsa.GenerateKey(opts.nextReader(), keylen) privkey, _ = rsa.GenerateKey(opts.myFooNextReader(), keylen)
if allowMocking { /*
privkey = maybeDerandomizeMockKey(privkey, keylen, opts) if 0 != opts.Seed {
for i := 0; i < maxRetry; i++ {
otherkey, _ := rsa.GenerateKey(opts.myFooNextReader(), keylen)
otherCmp := otherkey.D.Cmp(privkey.(*rsa.PrivateKey).D)
if 0 != otherCmp {
// There are two possible keys, choose the lesser D value
// See https://github.com/square/go-jose/issues/189
if otherCmp < 0 {
privkey = otherkey
} }
break
}
if maxRetry == i-1 {
log.Printf("error: coinflip landed on heads %d times", maxRetry)
}
}
}
*/
} else { } else {
// TODO: EC keys may also suffer the same random problems in the future // TODO: EC keys may also suffer the same random problems in the future
privkey, _ = ecdsa.GenerateKey(elliptic.P256(), opts.nextReader()) privkey, _ = ecdsa.GenerateKey(elliptic.P256(), opts.myFooNextReader())
} }
return privkey return privkey
} }

69
jwk.go
View File

@ -1,69 +0,0 @@
package keypairs
import (
"fmt"
)
// JWK abstracts EC and RSA keys
type JWK interface {
marshalJWK() ([]byte, error)
}
// ECJWK is the EC variant
type ECJWK struct {
KeyID string `json:"kid,omitempty"`
Curve string `json:"crv"`
X string `json:"x"`
Y string `json:"y"`
Use []string `json:"use,omitempty"`
Seed string `json:"_seed,omitempty"`
}
func (k *ECJWK) marshalJWK() ([]byte, error) {
return []byte(fmt.Sprintf(`{"crv":%q,"kty":"EC","x":%q,"y":%q}`, k.Curve, k.X, k.Y)), nil
}
// RSAJWK is the RSA variant
type RSAJWK struct {
KeyID string `json:"kid,omitempty"`
Exp string `json:"e"`
N string `json:"n"`
Use []string `json:"use,omitempty"`
Seed string `json:"_seed,omitempty"`
}
func (k *RSAJWK) marshalJWK() ([]byte, error) {
return []byte(fmt.Sprintf(`{"e":%q,"kty":"RSA","n":%q}`, k.Exp, k.N)), nil
}
/*
// ToPublicJWK exposes only the public parts
func ToPublicJWK(pubkey PublicKey) JWK {
switch k := pubkey.Key().(type) {
case *ecdsa.PublicKey:
return ECToPublicJWK(k)
case *rsa.PublicKey:
return RSAToPublicJWK(k)
default:
panic(errors.New("impossible key type"))
//return nil
}
}
// ECToPublicJWK will output the most minimal version of an EC JWK (no key id, no "use" flag, nada)
func ECToPublicJWK(k *ecdsa.PublicKey) *ECJWK {
return &ECJWK{
Curve: k.Curve.Params().Name,
X: base64.RawURLEncoding.EncodeToString(k.X.Bytes()),
Y: base64.RawURLEncoding.EncodeToString(k.Y.Bytes()),
}
}
// RSAToPublicJWK will output the most minimal version of an RSA JWK (no key id, no "use" flag, nada)
func RSAToPublicJWK(p *rsa.PublicKey) *RSAJWK {
return &RSAJWK{
Exp: base64.RawURLEncoding.EncodeToString(big.NewInt(int64(p.E)).Bytes()),
N: base64.RawURLEncoding.EncodeToString(p.N.Bytes()),
}
}
*/

63
jws.go
View File

@ -1,63 +0,0 @@
package keypairs
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"strings"
)
// JWS is a parsed JWT, representation as signable/verifiable and human-readable parts
type JWS struct {
Header Object `json:"header"` // JSON
Claims Object `json:"claims"` // JSON
Protected string `json:"protected"` // base64
Payload string `json:"payload"` // base64
Signature string `json:"signature"` // base64
}
// JWSToJWT joins JWS parts into a JWT as {ProtectedHeader}.{SerializedPayload}.{Signature}.
func JWSToJWT(jwt *JWS) string {
return fmt.Sprintf(
"%s.%s.%s",
jwt.Protected,
jwt.Payload,
jwt.Signature,
)
}
// JWTToJWS splits the JWT into its JWS segments
func JWTToJWS(jwt string) (jws *JWS) {
jwt = strings.TrimSpace(jwt)
parts := strings.Split(jwt, ".")
if 3 != len(parts) {
return nil
}
return &JWS{
Protected: parts[0],
Payload: parts[1],
Signature: parts[2],
}
}
// DecodeComponents decodes JWS Header and Claims
func (jws *JWS) DecodeComponents() error {
protected, err := base64.RawURLEncoding.DecodeString(jws.Protected)
if nil != err {
return errors.New("invalid JWS header base64Url encoding")
}
if err := json.Unmarshal([]byte(protected), &jws.Header); nil != err {
return errors.New("invalid JWS header")
}
payload, err := base64.RawURLEncoding.DecodeString(jws.Payload)
if nil != err {
return errors.New("invalid JWS payload base64Url encoding")
}
if err := json.Unmarshal([]byte(payload), &jws.Claims); nil != err {
return errors.New("invalid JWS claims")
}
return nil
}

View File

@ -25,8 +25,8 @@ import (
// TODO should be ErrInvalidJWKURL // TODO should be ErrInvalidJWKURL
// ErrInvalidJWKURL means that the url did not provide JWKs // EInvalidJWKURL means that the url did not provide JWKs
var ErrInvalidJWKURL = errors.New("url does not lead to valid JWKs") var EInvalidJWKURL = errors.New("url does not lead to valid JWKs")
// KeyCache is an in-memory key cache // 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.PublicKeyDeprecated Key keypairs.PublicKey
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.PublicKeyDeprecated type PublicKeysMap map[string]keypairs.PublicKey
// 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,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 // 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.PublicKeyDeprecated, error) { return immediateOneOrFetch(url, url, func(string) (map[string]map[string]string, map[string]keypairs.PublicKey, 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[keypairs.Thumbprint(key)] = m maps[key.Thumbprint()] = m
maps[url] = m maps[url] = m
keys := uncached.PublicKeysMap{} // map[string]keypairs.PublicKeyDeprecated{} keys := map[string]keypairs.PublicKey{}
keys[keypairs.Thumbprint(key)] = pubd keys[key.Thumbprint()] = key
keys[url] = pubd keys[url] = key
return maps, keys, nil 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" // 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, return immediateOneOrFetch(url, url, func(string) (map[string]map[string]string, map[string]keypairs.PublicKey, error) {
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
@ -177,10 +165,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[keypairs.Thumbprint(key.Key())] = m maps[key.Thumbprint()] = m
keys := map[string]keypairs.PublicKeyDeprecated{} keys := map[string]keypairs.PublicKey{}
keys[keypairs.Thumbprint(key.Key())] = key keys[key.Thumbprint()] = key
return maps, keys, nil 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. // 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.Key() return pub.Key
} }
return nil return nil
} }
@ -224,21 +212,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()
hit := get(kidOrThumb, iss) key := get(kidOrThumb, iss)
if nil == hit { if nil == key {
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 hit.Expiry.Sub(now) <= StaleTime { if key.Expiry.Sub(now) <= StaleTime {
go fetchAndSelect(kidOrThumb, iss, fetcher) 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) { func fetchAndSelect(id, baseURL string, fetcher myfetcher) (keypairs.PublicKey, error) {
maps, keys, err := fetcher(baseURL) maps, keys, err := fetcher(baseURL)
@ -249,21 +237,20 @@ 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 == keypairs.Thumbprint(pub) { if id == key.Thumbprint() {
return pub, nil return key, nil
} }
if id == key.KeyID() { 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) 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 { for i := range keys {
key := keys[i] key := keys[i]
m := maps[i] m := maps[i]
@ -273,13 +260,10 @@ func cacheKeys(maps map[string]map[string]string, keys PublicKeysMap, issuer str
} }
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.PublicKeyDeprecated) error { func cacheKey(kid, iss, expstr string, pub keypairs.PublicKey) error {
var expiry time.Time var expiry time.Time
iss = normalizeIssuer(iss) iss = normalizeIssuer(iss)
@ -303,7 +287,7 @@ func cacheKey(kid, iss, expstr string, pub keypairs.PublicKeyDeprecated) error {
Expiry: expiry, Expiry: expiry,
} }
// Since thumbprints are crypto secure, iss isn't needed // Since thumbprints are crypto secure, iss isn't needed
thumb := keypairs.Thumbprint(pub.Key()) thumb := pub.Thumbprint()
KeyCache[thumb] = CachableKey{ KeyCache[thumb] = CachableKey{
Key: pub, Key: pub,
Expiry: expiry, Expiry: expiry,

View File

@ -11,19 +11,16 @@ 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
pubk3, err := PEM("https://bigsquid.auth0.com/pem") k3, err := PEM("https://bigsquid.auth0.com/pem")
if nil != err { if nil != err {
t.Fatal("[0] Error fetching and caching key:", err) t.Fatal("Error fetching and caching key:", err)
} }
thumb3 := keypairs.Thumbprint(pubk3) if k3.Thumbprint() != pubkey.Thumbprint() {
thumb := keypairs.Thumbprint(pubkey) t.Fatal("Error got different thumbprint for different versions of the same key:", err)
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/")
@ -50,10 +47,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() key = keys[i]
break break
} }
thumb := keypairs.Thumbprint(key) thumb := key.Thumbprint()
// 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 {
@ -63,25 +60,20 @@ 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("[1] Error fetching and caching key:", err) t.Fatal("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() {
// TODO thumb / id mapping if pub := Get(pubkey.KeyID(), url); nil == pub {
thumb = keypairs.Thumbprint(pubkey) t.Fatal("key was not properly cached by kid", pubkey.KeyID())
if pub := Get(thumb, url); nil == pub {
t.Fatal("key was not properly cached by kid", pubkey)
} }
// TODO } else {
/* 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()
@ -94,10 +86,8 @@ 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.Equal(pubkey) || keypairs.Thumbprint(key) != keypairs.Thumbprint(pubkey) { if key.KeyID() != pubkey.KeyID() || key.Thumbprint() != pubkey.Thumbprint() {
t.Fatalf("SANITY: [todo: KeyIDs or] Thumbprints do not match:\n%q != %q\n%q != %q", t.Fatal("SANITY: KeyIDs or Thumbprints do not match:", key.KeyID(), pubkey.KeyID(), key.Thumbprint(), pubkey.Thumbprint())
keypairs.Thumbprint(key), keypairs.Thumbprint(pubkey),
keypairs.Thumbprint(key), keypairs.Thumbprint(pubkey))
} }
// Get 404 // Get 404

View File

@ -4,8 +4,6 @@ package uncached
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"crypto/rsa"
"crypto/ecdsa"
"errors" "errors"
"io" "io"
"io/ioutil" "io/ioutil"
@ -17,17 +15,8 @@ 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) (JWKMapByID, PublicKeysMap, error) { func OIDCJWKs(baseURL string) (map[string]map[string]string, map[string]keypairs.PublicKey, error) {
baseURL = normalizeBaseURL(baseURL) baseURL = normalizeBaseURL(baseURL)
oidcConf := struct { oidcConf := struct {
JWKSURI string `json:"jwks_uri"` 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 // 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) baseURL = normalizeBaseURL(baseURL)
url := baseURL + ".well-known/jwks.json" 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) // JWKs fetches and parses a jwks.json (assuming well-known format)
func JWKs(jwksurl string) (JWKMapByID, PublicKeysMap, error) { func JWKs(jwksurl string) (map[string]map[string]string, map[string]keypairs.PublicKey, error) {
keys := PublicKeysMap{} keys := map[string]keypairs.PublicKey{}
maps := JWKMapByID{} maps := map[string]map[string]string{}
resp := struct { resp := struct {
Keys []map[string]interface{} `json:"keys"` Keys []map[string]interface{} `json:"keys"`
}{ }{
@ -82,8 +71,8 @@ func JWKs(jwksurl string) (JWKMapByID, PublicKeysMap, error) {
if nil != err { if nil != err {
return nil, nil, err return nil, nil, err
} }
keys[keypairs.Thumbprint(key.Key())] = key keys[key.Thumbprint()] = key
maps[keypairs.Thumbprint(key.Key())] = m maps[key.Thumbprint()] = m
} }
return maps, keys, nil 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) // 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 pubd keypairs.PublicKeyDeprecated var pub keypairs.PublicKey
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
} }
pubd, err = keypairs.ParsePublicKey(pem) pub, 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"] = keypairs.Thumbprint(pub) m["kid"] = pemurl
// TODO is this just junk?
m[URLishKey] = pemurl
switch pub.(type) { switch p := pub.(type) {
case *ecdsa.PublicKey: case *keypairs.ECPublicKey:
//p.KID = pemurl p.KID = pemurl
case *rsa.PublicKey: case *keypairs.RSAPublicKey:
//p.KID = pemurl p.KID = pemurl
default: default:
return nil, nil, errors.New("impossible key type") 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) // 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{} 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)

View File

@ -3,6 +3,7 @@ package keypairs
import ( import (
"bytes" "bytes"
"crypto" "crypto"
"crypto/dsa"
"crypto/ecdsa" "crypto/ecdsa"
"crypto/elliptic" "crypto/elliptic"
"crypto/rsa" "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 // 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 is so that v0.7.x can use golang v1.15 keys // PublicKey thinly veils crypto.PublicKey for type safety
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
//Equal(x crypto.PublicKey) bool Thumbprint() string
//Thumbprint() string
KeyID() string KeyID() string
Key() PublicKey Key() crypto.PublicKey
ExpiresAt() time.Time ExpiresAt() time.Time
} }
@ -93,18 +87,13 @@ 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() PublicKey { func (p *ECPublicKey) Key() crypto.PublicKey {
return p.PublicKey return p.PublicKey
} }
@ -123,18 +112,13 @@ 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() PublicKey { func (p *RSAPublicKey) Key() crypto.PublicKey {
return p.PublicKey return p.PublicKey
} }
@ -149,13 +133,8 @@ 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) PublicKeyDeprecated { func NewPublicKey(pub crypto.PublicKey, kid ...string) PublicKey {
_, ok := pub.(PublicKey) var k 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{
@ -177,6 +156,14 @@ func NewPublicKey(pub crypto.PublicKey, kid ...string) PublicKeyDeprecated {
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))
} }
@ -188,11 +175,13 @@ func NewPublicKey(pub crypto.PublicKey, kid ...string) PublicKeyDeprecated {
// 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.(type) { switch k := key.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)
@ -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 // ThumbprintPublicKey returns the SHA256 RFC-spec JWK thumbprint
func ThumbprintPublicKey(pub PublicKeyDeprecated) string { func ThumbprintPublicKey(pub PublicKey) string {
return ThumbprintUntypedPublicKey(pub.Key()) 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). // (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 PublicKeyDeprecated: case PublicKey:
return ThumbprintUntypedPublicKey(p.Key()) return ThumbprintUntypedPublicKey(p.Key())
case *ecdsa.PublicKey: case *ecdsa.PublicKey:
return ThumbprintECPublicKey(p) return ThumbprintECPublicKey(p)
@ -379,7 +363,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) (PublicKeyDeprecated, error) { func ParsePublicKey(block []byte) (PublicKey, error) {
blocks, err := getPEMBytes(block) blocks, err := getPEMBytes(block)
if nil != err { if nil != err {
return nil, ErrParsePublicKey return nil, ErrParsePublicKey
@ -406,11 +390,11 @@ func ParsePublicKey(block []byte) (PublicKeyDeprecated, 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) (PublicKeyDeprecated, error) { func ParsePublicKeyString(block string) (PublicKey, error) {
return ParsePublicKey([]byte(block)) return ParsePublicKey([]byte(block))
} }
func parsePublicKey(der []byte) (PublicKeyDeprecated, error) { func parsePublicKey(der []byte) (PublicKey, 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) {
@ -456,7 +440,7 @@ func parsePublicKey(der []byte) (PublicKeyDeprecated, 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) (PublicKeyDeprecated, error) { func NewJWKPublicKey(m map[string]string) (PublicKey, error) {
switch m["kty"] { switch m["kty"] {
case "RSA": case "RSA":
return parseRSAPublicKey(m) 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 // 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 // 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
@ -477,7 +461,7 @@ func ParseJWKPublicKey(b []byte) (PublicKeyDeprecated, 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) (PublicKeyDeprecated, error) { func ParseJWKPublicKeyString(s string) (PublicKey, error) {
if strings.Contains(s, `"d"`) { if strings.Contains(s, `"d"`) {
return nil, ErrUnexpectedPrivateKey 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 // 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) 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
@ -497,7 +481,7 @@ func DecodeJWKPublicKey(r io.Reader) (PublicKeyDeprecated, 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{}) (PublicKeyDeprecated, error) { func newJWKPublicKey(data interface{}) (PublicKey, error) {
var m map[string]string var m map[string]string
switch d := data.(type) { 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 // 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.PublicKeyDeprecated Keys []keypairs.PublicKey
ExpiresIn time.Duration 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) jwks := make([]string, 0, 1)
for i := range keys { 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 // 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.
pub := key.Key() jwk := string(keypairs.MarshalJWKPublicKey(key, exp))
jwk := string(keypairs.MarshalJWKPublicKey(pub, exp))
jwks = append(jwks, jwk) jwks = append(jwks, jwk)
} }

View File

@ -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.PublicKeyDeprecated{ pubs := []keypairs.PublicKey{
keypairs.NewPublicKey(eckey.Public()), keypairs.NewPublicKey(eckey.Public()),
} }
@ -42,9 +42,8 @@ func TestServeKeys(t *testing.T) {
go func() { go func() {
time.Sleep(15 * time.Second) 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) h.Shutdown(ctx)
cancel()
}() }()
m := map[string]string{} m := map[string]string{}

View File

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

46
mock.go
View File

@ -1,46 +0,0 @@
package keypairs
import (
"crypto/rsa"
"io"
"log"
mathrand "math/rand"
)
// this shananigans is only for testing and debug API stuff
func (o *keyOptions) maybeMockReader() io.Reader {
if !allowMocking {
panic("mock method called when mocking is not allowed")
}
if 0 == o.mockSeed {
return randReader
}
log.Println("WARNING: MOCK: using insecure reader")
return mathrand.New(mathrand.NewSource(o.mockSeed))
}
const maxRetry = 16
func maybeDerandomizeMockKey(privkey PrivateKey, keylen int, opts *keyOptions) PrivateKey {
if 0 != opts.mockSeed {
for i := 0; i < maxRetry; i++ {
otherkey, _ := rsa.GenerateKey(opts.nextReader(), keylen)
otherCmp := otherkey.D.Cmp(privkey.(*rsa.PrivateKey).D)
if 0 != otherCmp {
// There are two possible keys, choose the lesser D value
// See https://github.com/square/go-jose/issues/189
if otherCmp < 0 {
privkey = otherkey
}
break
}
if maxRetry == i-1 {
log.Printf("error: coinflip landed on heads %d times", maxRetry)
}
}
}
return privkey
}

165
sign.go
View File

@ -1,165 +0,0 @@
package keypairs
import (
"crypto"
"crypto/ecdsa"
"crypto/rsa"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
mathrand "math/rand" // to be used for good, not evil
"time"
)
// Object is a type alias representing generic JSON data
type Object = map[string]interface{}
// SignClaims adds `typ`, `kid` (or `jwk`), and `alg` in the header and expects claims for `jti`, `exp`, `iss`, and `iat`
func SignClaims(privkey PrivateKey, header Object, claims Object) (*JWS, error) {
var randsrc io.Reader = randReader
seed, _ := header["_seed"].(int64)
if 0 != seed {
randsrc = mathrand.New(mathrand.NewSource(seed))
//delete(header, "_seed")
}
protected, header, err := headerToProtected(privkey.Public().(PublicKey), header)
if nil != err {
return nil, err
}
protected64 := base64.RawURLEncoding.EncodeToString(protected)
payload, err := claimsToPayload(claims)
if nil != err {
return nil, err
}
payload64 := base64.RawURLEncoding.EncodeToString(payload)
signable := fmt.Sprintf(`%s.%s`, protected64, payload64)
hash := sha256.Sum256([]byte(signable))
sig := Sign(privkey, hash[:], randsrc)
sig64 := base64.RawURLEncoding.EncodeToString(sig)
//log.Printf("\n(Sign)\nSignable: %s", signable)
//log.Printf("Hash: %s", hash)
//log.Printf("Sig: %s", sig64)
return &JWS{
Header: header,
Claims: claims,
Protected: protected64,
Payload: payload64,
Signature: sig64,
}, nil
}
func headerToProtected(pub PublicKey, header Object) ([]byte, Object, error) {
if nil == header {
header = Object{}
}
// Only supporting 2048-bit and P256 keys right now
// because that's all that's practical and well-supported.
// No security theatre here.
alg := "ES256"
switch pub.(type) {
case *rsa.PublicKey:
alg = "RS256"
}
if selfSign, _ := header["_jwk"].(bool); selfSign {
delete(header, "_jwk")
any := Object{}
_ = json.Unmarshal(MarshalJWKPublicKey(pub), &any)
header["jwk"] = any
}
// TODO what are the acceptable values? JWT. JWS? others?
header["typ"] = "JWT"
if _, ok := header["jwk"]; !ok {
thumbprint := ThumbprintPublicKey(NewPublicKey(pub))
kid, _ := header["kid"].(string)
if "" != kid && thumbprint != kid {
return nil, nil, errors.New("'kid' should be the key's thumbprint")
}
header["kid"] = thumbprint
}
header["alg"] = alg
protected, err := json.Marshal(header)
if nil != err {
return nil, nil, err
}
return protected, header, nil
}
func claimsToPayload(claims Object) ([]byte, error) {
if nil == claims {
claims = Object{}
}
var dur time.Duration
jti, _ := claims["jti"].(string)
insecure, _ := claims["insecure"].(bool)
switch exp := claims["exp"].(type) {
case time.Duration:
// TODO: MUST this go first?
// int64(time.Duration) vs time.Duration(int64)
dur = exp
case string:
var err error
dur, err = time.ParseDuration(exp)
// TODO s, err := time.ParseDuration(dur)
if nil != err {
return nil, err
}
case int:
dur = time.Second * time.Duration(exp)
case int64:
dur = time.Second * time.Duration(exp)
case float64:
dur = time.Second * time.Duration(exp)
default:
dur = 0
}
if "" == jti && 0 == dur && !insecure {
return nil, errors.New("token must have jti or exp as to be expirable / cancellable")
}
claims["exp"] = time.Now().Add(dur).Unix()
return json.Marshal(claims)
}
// Sign signs both RSA and ECDSA. Use `nil` or `crypto/rand.Reader` except for debugging.
func Sign(privkey PrivateKey, hash []byte, rand io.Reader) []byte {
if nil == rand {
rand = randReader
}
var sig []byte
if len(hash) != 32 {
panic("only 256-bit hashes for 2048-bit and 256-bit keys are supported")
}
switch k := privkey.(type) {
case *rsa.PrivateKey:
sig, _ = rsa.SignPKCS1v15(rand, k, crypto.SHA256, hash)
case *ecdsa.PrivateKey:
r, s, _ := ecdsa.Sign(rand, k, hash[:])
rb := r.Bytes()
for len(rb) < 32 {
rb = append([]byte{0}, rb...)
}
sb := s.Bytes()
for len(rb) < 32 {
sb = append([]byte{0}, sb...)
}
sig = append(rb, sb...)
}
return sig
}

174
verify.go
View File

@ -1,174 +0,0 @@
package keypairs
import (
"crypto"
"crypto/ecdsa"
"crypto/rsa"
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
"errors"
"fmt"
"log"
"math/big"
"time"
)
// VerifyClaims will check the signature of a parsed JWT
func VerifyClaims(pubkey PublicKey, jws *JWS) (errs []error) {
kid, _ := jws.Header["kid"].(string)
jwkmap, hasJWK := jws.Header["jwk"].(Object)
//var jwk JWK = nil
seed, _ := jws.Header["_seed"].(int64)
seedf64, _ := jws.Header["_seed"].(float64)
kty, _ := jws.Header["_kty"].(string)
if 0 == seed {
seed = int64(seedf64)
}
var pub PublicKey = nil
if hasJWK {
pub, errs = selfsignCheck(jwkmap, errs)
} else {
opts := &keyOptions{mockSeed: seed, KeyType: kty}
pub, errs = pubkeyCheck(pubkey, kid, opts, errs)
}
jti, _ := jws.Claims["jti"].(string)
expf64, _ := jws.Claims["exp"].(float64)
exp := int64(expf64)
if 0 == exp {
if "" == jti {
err := errors.New("one of 'jti' or 'exp' must exist for token expiry")
errs = append(errs, err)
}
} else {
if time.Now().Unix() > exp {
err := fmt.Errorf("token expired at %d (%s)", exp, time.Unix(exp, 0))
errs = append(errs, err)
}
}
signable := fmt.Sprintf("%s.%s", jws.Protected, jws.Payload)
hash := sha256.Sum256([]byte(signable))
sig, err := base64.RawURLEncoding.DecodeString(jws.Signature)
if nil != err {
err := fmt.Errorf("could not decode signature: %w", err)
errs = append(errs, err)
return errs
}
//log.Printf("\n(Verify)\nSignable: %s", signable)
//log.Printf("Hash: %s", hash)
//log.Printf("Sig: %s", jws.Signature)
if nil == pub {
err := fmt.Errorf("token signature could not be verified")
errs = append(errs, err)
} else if !Verify(pub, hash[:], sig) {
err := fmt.Errorf("token signature is not valid")
errs = append(errs, err)
}
return errs
}
func selfsignCheck(jwkmap Object, errs []error) (PublicKey, []error) {
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[\"iss\"]")
kty := jwkmap["kty"]
var err error
if "RSA" == kty {
e, _ := jwkmap["e"].(string)
n, _ := jwkmap["n"].(string)
k, _ := (&RSAJWK{
Exp: e,
N: n,
}).marshalJWK()
pub, err = ParseJWKPublicKey(k)
if nil != err {
return nil, append(errs, err)
}
} else {
crv, _ := jwkmap["crv"].(string)
x, _ := jwkmap["x"].(string)
y, _ := jwkmap["y"].(string)
k, _ := (&ECJWK{
Curve: crv,
X: x,
Y: y,
}).marshalJWK()
pub, err = ParseJWKPublicKey(k)
if nil != err {
return nil, append(errs, err)
}
}
return pub.Key(), errs
}
func pubkeyCheck(pubkey PublicKey, kid string, opts *keyOptions, errs []error) (PublicKey, []error) {
var pub PublicKey = nil
if "" == kid {
err := errors.New("token should have 'kid' or 'jwk' in header to identify the public key")
errs = append(errs, err)
}
if nil == pubkey {
if allowMocking {
if 0 == opts.mockSeed {
err := errors.New("the debug API requires '_seed' to accompany 'kid'")
errs = append(errs, err)
}
if "" == opts.KeyType {
err := errors.New("the debug API requires '_kty' to accompany '_seed'")
errs = append(errs, err)
}
if 0 == opts.mockSeed || "" == opts.KeyType {
return nil, errs
}
privkey := newPrivateKey(opts)
pub = privkey.Public().(PublicKey)
return pub, errs
}
err := errors.New("no matching public key")
errs = append(errs, err)
} else {
pub = pubkey
}
if nil != pub && "" != kid {
if 1 != subtle.ConstantTimeCompare([]byte(kid), []byte(Thumbprint(pub))) {
err := errors.New("'kid' does not match the public key thumbprint")
errs = append(errs, err)
}
}
return pub, errs
}
// Verify will check the signature of a hash
func Verify(pubkey PublicKey, hash []byte, sig []byte) bool {
switch pub := pubkey.(type) {
case *rsa.PublicKey:
//log.Printf("RSA VERIFY")
// TODO Size(key) to detect key size ?
//alg := "SHA256"
// TODO: this hasn't been tested yet
if err := rsa.VerifyPKCS1v15(pub, crypto.SHA256, hash, sig); nil != err {
return false
}
return true
case *ecdsa.PublicKey:
r := &big.Int{}
r.SetBytes(sig[0:32])
s := &big.Int{}
s.SetBytes(sig[32:])
return ecdsa.Verify(pub, hash, r, s)
default:
panic("impossible condition: non-rsa/non-ecdsa key")
//return false
}
}