AJ ONeal
4 years ago
8 changed files with 559 additions and 84 deletions
@ -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 |
@ -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 |
|||
} |
@ -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