WIP (broken) add verify

This commit is contained in:
AJ ONeal 2020-08-05 08:13:32 +00:00
parent a1b4ad1202
commit e8c50dee76
6 changed files with 346 additions and 0 deletions

View File

@ -33,6 +33,12 @@ func (o *options) nextReader() io.Reader {
return rand.New(rand.NewSource(o.Seed))
}
/*
func getJWS(r *http.Request) (*options, error) {
}
*/
func getOpts(r *http.Request) (*options, error) {
tok := make(map[string]interface{})
decoder := json.NewDecoder(r.Body)

87
mockid/api/verify.go Normal file
View File

@ -0,0 +1,87 @@
package api
import (
"encoding/base64"
"encoding/json"
"io"
"log"
"net/http"
"strings"
"git.coolaj86.com/coolaj86/go-mockid/xkeypairs"
)
// VerifyJWT will verify both JWT and uncompressed JWS
func Verify(w http.ResponseWriter, r *http.Request) {
if "POST" != r.Method {
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
return
}
var jws *xkeypairs.JWS
authzParts := strings.Split(r.Header.Get("Authorization"), " ")
lenAuthz := len(authzParts)
if 2 == lenAuthz {
jwt := authzParts[1]
jwsParts := strings.Split(jwt, ".")
if 3 == len(jwsParts) {
jws = &xkeypairs.JWS{
Protected: jwsParts[0],
Payload: jwsParts[1],
Signature: jwsParts[2],
}
}
}
if nil == jws {
if 0 != lenAuthz {
http.Error(w, "Bad Request: malformed Authorization header", http.StatusBadRequest)
return
}
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&jws)
if nil != err && io.EOF != err {
log.Printf("json decode error: %s", err)
http.Error(w, "Bad Request: invalid JWS body", http.StatusBadRequest)
return
}
defer r.Body.Close()
}
protected, err := base64.RawURLEncoding.DecodeString(jws.Protected)
if nil != err {
http.Error(w, "Bad Request: invalid JWS header base64Url encoding", http.StatusBadRequest)
return
}
if err := json.Unmarshal([]byte(protected), &jws.Header); nil != err {
log.Printf("json decode error: %s", err)
http.Error(w, "Bad Request: invalid JWS header", http.StatusBadRequest)
return
}
payload, err := base64.RawURLEncoding.DecodeString(jws.Payload)
if nil != err {
http.Error(w, "Bad Request: invalid JWS payload base64Url encoding", http.StatusBadRequest)
return
}
if err := json.Unmarshal([]byte(payload), &jws.Claims); nil != err {
log.Printf("json decode error: %s", err)
http.Error(w, "Bad Request: invalid JWS claims", http.StatusBadRequest)
return
}
ok, err := xkeypairs.VerifyClaims(nil, jws)
if nil != err {
log.Printf("jws verify error: %s", err)
http.Error(w, "Bad Request: could not verify JWS claims", http.StatusBadRequest)
return
}
if !ok {
http.Error(w, "Bad Request: invalid JWS signature", http.StatusBadRequest)
return
}
b := []byte(`{"success":true}`)
w.Write(append(b, '\n'))
}

View File

@ -64,6 +64,39 @@ func TestMain(m *testing.M) {
//func TestSelfSignWithoutExp(t *testing.T)
//func TestSelfSignWithJTIWithoutExp(t *testing.T)
func TestVerifySelfSignedJWT(t *testing.T) {
jwt := "eyJfc2VlZCI6LTEzMDY3NDU1MDQxNDQsImFsZyI6IlJTMjU2IiwiandrIjp7ImUiOiJBUUFCIiwia2lkIjoiSEZ4ZTlGV1dVc2N3bjltaVozSXNJeWMwMjMtbEJ1UmtvOEJpVV9IRG9KOCIsImt0eSI6IlJTQSIsIm4iOiJ2NUZkSTdYaC0wekxWVEVQZl94ekdIUVpDcEZ2MWR2N2h3eHhrVjctYmxpYmt6LXIxUG9lZ3lQYzFXMjZlWFBvd0xQQXQ3a3dHQnVOdjdMVjh5MEtvMkxOZklaXzRILW54SkJPaWIybXlHOVVfQ29WRDBiM3NBWTdmcDd2QlV1bTBXYVM4R3hZOGtYU0ZOS0VTY0NDNVBpSmFyblNISk1PcUdIVm51YmpsSjl5c1NyNmNsaGpxc0R4dU9qOHpxamF2MUFxek1STWVpRl9CREJsOUFoUGNZSHpHN0JtaXB5UEo2XzBwdWNLTi0tUDZDRk92d05SVGx2ek41RmlRM3VHcy1fMHcwQzVMZWJ6N21BNmJNTFdXc0tRRFBvb3cxallCWHJKdVF1WkZoSmxLMmdidm9ZcV85dWhfLUM1Z3pPZnR4UHBCNnhtY3RfelVaeUdwUUxnQlEiLCJ1c2UiOiJzaWcifSwidHlwIjoiSldUIn0.eyJleHAiOjE1OTY2MTQ3NTYsInN1YiI6ImJhbmFuYXMifQ.qHpzlglOfZMzE3CTNAUXld_wC62JTAJuoQfMaNeFa-XPtYB2Maj8_w3YmRZg_q5S6y9ToCmZ8nWd1kuMheA5qBKOUQeQH47Jts5zWLd0UBckIHo5lK4mk0bUWuiNgr7c9DY6k1DIdFaavyWCXbhFwG0X83qlMhQlPh02dDpCuU78Nn2hF3mZETQKpBIVESYtfeU1Xy3OU_am0kwcN2klLcdweOcrLx_ONfcvAGY3KiIdFiz0ViySAsQ39BiSSvoDYqOOOi41Hky67bnyZQOdalQC_95McTeXApzmGXRUE74Gj-S8c9e5it5d4QZLPaQ1JHzUKz1s7TPvThIn58NA-g"
client := srv.Client()
urlstr, _ := url.Parse(srv.URL + "/verify")
req := &http.Request{
Method: "POST",
URL: urlstr,
//Body: ioutil.NopCloser(bytes.NewReader(jws)),
Header: http.Header{},
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", jwt))
res, err := client.Do(req)
if nil != err {
t.Error(err)
return
}
data, err := ioutil.ReadAll(res.Body)
if nil != err {
t.Error(err)
return
}
if 200 != res.StatusCode {
log.Printf(string(data))
t.Error(fmt.Errorf("bad status code: %d", res.StatusCode))
return
}
log.Printf("TODO: verify, and verify non-self-signed")
log.Printf(string(data))
}
func TestSelfSign(t *testing.T) {
client := srv.Client()
//urlstr, _ := url.Parse(srv.URL + "/jose.jws.json")

View File

@ -192,6 +192,7 @@ func Route(jwksPrefix string, privkey keypairs.PrivateKey) http.Handler {
http.HandleFunc("/jose.jws.json", api.SignJWS)
http.HandleFunc("/jose.jws.jwt", api.SignJWT)
http.HandleFunc("/verify", api.Verify)
http.HandleFunc("/inspect_token", func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")

70
xkeypairs/jwk.go Normal file
View File

@ -0,0 +1,70 @@
package xkeypairs
import (
"crypto/ecdsa"
"crypto/rsa"
"encoding/base64"
"errors"
"fmt"
"math/big"
"git.rootprojects.org/root/keypairs"
)
type JWK interface {
marshalJWK() ([]byte, error)
}
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
}
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
}
func ToPublicJWK(pubkey keypairs.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()),
}
}

149
xkeypairs/verify.go Normal file
View File

@ -0,0 +1,149 @@
package xkeypairs
import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rsa"
"crypto/sha256"
"encoding/base64"
"errors"
"fmt"
"io"
"log"
"math/big"
mathrand "math/rand"
"git.rootprojects.org/root/keypairs"
)
func VerifyClaims(pubkey keypairs.PublicKey, jws *JWS) (bool, error) {
seed, _ := jws.Header["_seed"].(int64)
kty, _ := jws.Header["_kty"].(string)
kid, _ := jws.Header["kid"].(string)
jwkmap, hasJWK := jws.Header["jwk"].(Object)
//var jwk JWK = nil
var pub keypairs.PublicKey = nil
if hasJWK {
fmt.Println("Security TODO: did not check jws.Claims[\"sub\"] against 'jwk' thumbprint")
fmt.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
}
fmt.Println("Security TODO: did not check jws.Claims[\"kid\"] against thumbprint")
}
hash := sha256.Sum256([]byte(fmt.Sprintf("%s.%s", jws.Protected, jws.Payload)))
sig, err := base64.RawURLEncoding.DecodeString(jws.Signature)
if nil != err {
return false, err
}
return Verify(pub, hash[:], sig), nil
}
func Verify(pubkey keypairs.PublicKey, hash []byte, sig []byte) bool {
var verified bool
switch pub := pubkey.Key().(type) {
case *rsa.PublicKey:
// 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 {
verified = true
}
case *ecdsa.PublicKey:
r := &big.Int{}
r.SetBytes(sig[0:32])
s := &big.Int{}
s.SetBytes(sig[32:])
fmt.Println("debug: sig len:", len(sig))
fmt.Println("debug: r, s:", r, s)
verified = ecdsa.Verify(pub, hash, r, s)
default:
panic("impossible condition: non-rsa/non-ecdsa key")
}
return verified
}
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)
}
}
}
} 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))
}