173 lines
		
	
	
		
			4.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			173 lines
		
	
	
		
			4.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package xkeypairs
 | |
| 
 | |
| import (
 | |
| 	"crypto"
 | |
| 	"crypto/ecdsa"
 | |
| 	"crypto/elliptic"
 | |
| 	"crypto/rsa"
 | |
| 	"crypto/sha256"
 | |
| 	"encoding/base64"
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"log"
 | |
| 	"math/big"
 | |
| 	mathrand "math/rand"
 | |
| 	"time"
 | |
| 
 | |
| 	"git.rootprojects.org/root/keypairs"
 | |
| )
 | |
| 
 | |
| func VerifyClaims(pubkey keypairs.PublicKey, jws *JWS) (bool, error) {
 | |
| 	seed, _ := jws.Header["_seed"].(int64)
 | |
| 	seedf64, _ := jws.Header["_seed"].(float64)
 | |
| 	kty, _ := jws.Header["_kty"].(string)
 | |
| 	kid, _ := jws.Header["kid"].(string)
 | |
| 	jwkmap, hasJWK := jws.Header["jwk"].(Object)
 | |
| 	//var jwk JWK = nil
 | |
| 
 | |
| 	if 0 == seed {
 | |
| 		seed = int64(seedf64)
 | |
| 	}
 | |
| 
 | |
| 	var pub keypairs.PublicKey = nil
 | |
| 	if hasJWK {
 | |
| 		log.Println("Security TODO: did not check jws.Claims[\"sub\"] against 'jwk' thumbprint")
 | |
| 		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 = keypairs.ParseJWKPublicKey(k)
 | |
| 			if nil != err {
 | |
| 				return false, 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 = keypairs.ParseJWKPublicKey(k)
 | |
| 			if nil != err {
 | |
| 				return false, err
 | |
| 			}
 | |
| 		}
 | |
| 	} else {
 | |
| 		if "" == kid {
 | |
| 			return false, errors.New("token should have 'kid' or 'jwk' in header")
 | |
| 		}
 | |
| 		if nil == pubkey {
 | |
| 			if 0 == seed {
 | |
| 				return false, errors.New("the debug API requires '_seed' to accompany 'kid'")
 | |
| 			}
 | |
| 			if "" == kty {
 | |
| 				return false, errors.New("the debug API requires '_kty' to accompany '_seed'")
 | |
| 			}
 | |
| 			privkey := genPrivKey(seed, kty)
 | |
| 			pub = keypairs.NewPublicKey(privkey.Public())
 | |
| 		} else {
 | |
| 			pub = pubkey
 | |
| 		}
 | |
| 		log.Println("Security TODO: did not check jws.Claims[\"kid\"] against thumbprint")
 | |
| 	}
 | |
| 
 | |
| 	jti, _ := jws.Claims["jti"].(string)
 | |
| 	expf64, _ := jws.Claims["exp"].(float64)
 | |
| 	exp := int64(expf64)
 | |
| 	if 0 == exp {
 | |
| 		if "" == jti {
 | |
| 			return false, errors.New("one of 'jti' or 'exp' must exist for token expiry")
 | |
| 		}
 | |
| 	} else {
 | |
| 		if time.Now().Unix() > exp {
 | |
| 			return false, fmt.Errorf("token expired at %d (%s)", exp, time.Unix(exp, 0))
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	signable := fmt.Sprintf("%s.%s", jws.Protected, jws.Payload)
 | |
| 	hash := sha256.Sum256([]byte(signable))
 | |
| 	sig, err := base64.RawURLEncoding.DecodeString(jws.Signature)
 | |
| 	if nil != err {
 | |
| 		return false, err
 | |
| 	}
 | |
| 	//log.Printf("\n(Verify)\nSignable: %s", signable)
 | |
| 	//log.Printf("Hash: %s", hash)
 | |
| 	//log.Printf("Sig: %s", jws.Signature)
 | |
| 
 | |
| 	return Verify(pub, hash[:], sig), nil
 | |
| }
 | |
| 
 | |
| func Verify(pubkey keypairs.PublicKey, hash []byte, sig []byte) bool {
 | |
| 
 | |
| 	switch pub := pubkey.Key().(type) {
 | |
| 	case *rsa.PublicKey:
 | |
| 		//log.Printf("RSA VERIFY")
 | |
| 		// TODO keypairs.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
 | |
| 	}
 | |
| }
 | |
| 
 | |
| const maxRetry = 16
 | |
| 
 | |
| func genPrivKey(seed int64, kty string) keypairs.PrivateKey {
 | |
| 	var privkey keypairs.PrivateKey
 | |
| 
 | |
| 	if "RSA" == kty {
 | |
| 		keylen := 2048
 | |
| 		privkey, _ = rsa.GenerateKey(nextReader(seed), keylen)
 | |
| 		if 0 != seed {
 | |
| 			for i := 0; i < maxRetry; i++ {
 | |
| 				otherkey, _ := rsa.GenerateKey(nextReader(seed), 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)
 | |
| 					// TODO return random / retry error
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	} else {
 | |
| 		// TODO: EC keys may also suffer the same random problems in the future
 | |
| 		privkey, _ = ecdsa.GenerateKey(elliptic.P256(), nextReader(seed))
 | |
| 	}
 | |
| 	return privkey
 | |
| }
 | |
| 
 | |
| // this shananigans is only for testing and debug API stuff
 | |
| func nextReader(seed int64) io.Reader {
 | |
| 	if 0 == seed {
 | |
| 		return RandomReader
 | |
| 	}
 | |
| 	return mathrand.New(mathrand.NewSource(seed))
 | |
| }
 |