add verify subcommand
This commit is contained in:
parent
a560441c61
commit
d42763516f
|
@ -0,0 +1,19 @@
|
|||
#!/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
|
|
@ -30,13 +30,17 @@ func usage() {
|
|||
fmt.Println(" version")
|
||||
fmt.Println(" gen")
|
||||
fmt.Println(" sign")
|
||||
fmt.Println(" verify")
|
||||
fmt.Println("")
|
||||
fmt.Println("Examples:")
|
||||
fmt.Println(" keypairs gen -o key.jwk.json [--pub <public-key>]")
|
||||
fmt.Println("")
|
||||
fmt.Println(" keypairs sign --exp 15m key.jwk.json payload.json")
|
||||
fmt.Println(" keypairs sign --exp 15m key.jwk.json '{ \"sub\": \"xxxx\" }'")
|
||||
fmt.Println("")
|
||||
//fmt.Println(" verify")
|
||||
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() {
|
||||
|
@ -65,9 +69,11 @@ func main() {
|
|||
os.Exit(0)
|
||||
return
|
||||
case "gen":
|
||||
gen(args)
|
||||
gen(args[2:])
|
||||
case "sign":
|
||||
sign(args)
|
||||
sign(args[2:])
|
||||
case "verify":
|
||||
verify(args[2:])
|
||||
default:
|
||||
usage()
|
||||
os.Exit(1)
|
||||
|
@ -94,42 +100,27 @@ func sign(args []string) {
|
|||
flags := flag.NewFlagSet("sign", flag.ExitOnError)
|
||||
flags.DurationVar(&exp, "exp", 0, "duration until token expires (Default 15m)")
|
||||
flags.Parse(args)
|
||||
if len(flags.Args()) <= 3 {
|
||||
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()[2]
|
||||
payload := flags.Args()[3]
|
||||
keyname := flags.Args()[0]
|
||||
payload := flags.Args()[1]
|
||||
|
||||
var key keypairs.PrivateKey = nil
|
||||
b, err := ioutil.ReadFile(keyname)
|
||||
key, err := readKey(keyname)
|
||||
if nil != err {
|
||||
var err2 error
|
||||
key, err2 = keypairs.ParsePrivateKey([]byte(keyname))
|
||||
if nil != err2 {
|
||||
fmt.Fprintf(os.Stderr,
|
||||
"could not read private key as file (or parse as string) %q: %s\n", keyname, err)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||
os.Exit(1)
|
||||
return
|
||||
}
|
||||
if nil == key {
|
||||
var err3 error
|
||||
key, err3 = keypairs.ParsePrivateKey(b)
|
||||
if nil != err3 {
|
||||
fmt.Fprintf(os.Stderr,
|
||||
"could not parse private key from file %q: %s\n", keyname, err3)
|
||||
os.Exit(1)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if "" == payload {
|
||||
// TODO should this be null? I forget
|
||||
payload = "{}"
|
||||
}
|
||||
|
||||
b, err = ioutil.ReadFile(payload)
|
||||
b, err := ioutil.ReadFile(payload)
|
||||
claims := map[string]interface{}{}
|
||||
if nil != err {
|
||||
var err2 error
|
||||
|
@ -167,8 +158,159 @@ func sign(args []string) {
|
|||
}
|
||||
|
||||
b, _ = json.Marshal(&jws)
|
||||
fmt.Printf("JWS:\n%s\n\n", indentJSON(b))
|
||||
fmt.Printf("JWT:\n%s\n\n", keypairs.JWSToJWT(jws))
|
||||
fmt.Fprintf(os.Stderr, "%s\n", indentJSON(b))
|
||||
fmt.Fprintf(os.Stdout, "%s\n", keypairs.JWSToJWT(jws))
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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!
|
||||
var err2 error
|
||||
pub, err2 = keypairs.ParsePublicKey([]byte(pubname))
|
||||
if nil != err2 {
|
||||
return nil, fmt.Errorf(
|
||||
"could not read public key as file (or parse as string) %q:\n%w",
|
||||
pubname, err,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Oh, it was a file.
|
||||
if nil == pub {
|
||||
var err3 error
|
||||
pub, err3 = keypairs.ParsePublicKey(b)
|
||||
if nil != err3 {
|
||||
return nil, fmt.Errorf(
|
||||
"could not parse public key from file %q:\n%w",
|
||||
pubname, err3,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
|
|
46
generate.go
46
generate.go
|
@ -11,34 +11,36 @@ import (
|
|||
)
|
||||
|
||||
var randReader io.Reader = rand.Reader
|
||||
var maxRetry = 1
|
||||
var allowMocking = false
|
||||
|
||||
// KeyOptions are the things that we may need to know about a request to fulfill it properly
|
||||
type keyOptions struct {
|
||||
//Key string `json:"key"`
|
||||
KeyType string `json:"kty"`
|
||||
//Seed int64 `json:"-"`
|
||||
mockSeed int64 //`json:"-"`
|
||||
//SeedStr string `json:"seed"`
|
||||
//Claims Object `json:"claims"`
|
||||
//Header Object `json:"header"`
|
||||
}
|
||||
|
||||
// this shananigans is only for testing and debug API stuff
|
||||
func (o *keyOptions) myFooNextReader() io.Reader {
|
||||
return randReader
|
||||
/*
|
||||
if 0 == o.Seed {
|
||||
return randReader
|
||||
func (o *keyOptions) nextReader() io.Reader {
|
||||
if allowMocking {
|
||||
return o.maybeMockReader()
|
||||
}
|
||||
return mathrand.New(mathrand.NewSource(o.Seed))
|
||||
*/
|
||||
return randReader
|
||||
}
|
||||
|
||||
// NewDefaultPrivateKey generates a key with reasonable strength.
|
||||
// Today that means a 256-bit equivalent - either RSA 2048 or EC P-256.
|
||||
func NewDefaultPrivateKey() PrivateKey {
|
||||
// insecure random is okay here,
|
||||
// it's just used for a coin toss
|
||||
mathrand.Seed(time.Now().UnixNano())
|
||||
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 {
|
||||
return newPrivateKey(&keyOptions{
|
||||
KeyType: "RSA",
|
||||
|
@ -55,29 +57,13 @@ func newPrivateKey(opts *keyOptions) PrivateKey {
|
|||
|
||||
if "RSA" == opts.KeyType {
|
||||
keylen := 2048
|
||||
privkey, _ = rsa.GenerateKey(opts.myFooNextReader(), keylen)
|
||||
/*
|
||||
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
|
||||
privkey, _ = rsa.GenerateKey(opts.nextReader(), keylen)
|
||||
if allowMocking {
|
||||
privkey = maybeDerandomizeMockKey(privkey, keylen, opts)
|
||||
}
|
||||
break
|
||||
}
|
||||
if maxRetry == i-1 {
|
||||
log.Printf("error: coinflip landed on heads %d times", maxRetry)
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
} else {
|
||||
// TODO: EC keys may also suffer the same random problems in the future
|
||||
privkey, _ = ecdsa.GenerateKey(elliptic.P256(), opts.myFooNextReader())
|
||||
privkey, _ = ecdsa.GenerateKey(elliptic.P256(), opts.nextReader())
|
||||
}
|
||||
return privkey
|
||||
}
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
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()),
|
||||
}
|
||||
}
|
||||
*/
|
|
@ -0,0 +1,63 @@
|
|||
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
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
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
|
||||
}
|
26
sign.go
26
sign.go
|
@ -10,24 +10,10 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
mathrand "math/rand"
|
||||
mathrand "math/rand" // to be used for good, not evil
|
||||
"time"
|
||||
)
|
||||
|
||||
// randReader may be overwritten for testing
|
||||
//var randReader io.Reader = rand.Reader
|
||||
|
||||
//var randReader = rand.Reader
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Object is a type alias representing generic JSON data
|
||||
type Object = map[string]interface{}
|
||||
|
||||
|
@ -149,16 +135,6 @@ func claimsToPayload(claims Object) ([]byte, error) {
|
|||
return json.Marshal(claims)
|
||||
}
|
||||
|
||||
// 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,
|
||||
)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
|
|
|
@ -0,0 +1,174 @@
|
|||
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 PublicKey = nil
|
||||
log.Println("Security TODO: did not check jws.Claims[\"sub\"] against 'jwk'")
|
||||
log.Println("Security TODO: did not check jws.Claims[\"iss\"]")
|
||||
kty := jwkmap["kty"]
|
||||
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, 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 = NewPublicKey(privkey.Public())
|
||||
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(pub.Thumbprint())) {
|
||||
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.Key().(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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue