From e8c50dee76b6a063ae586ae699e6ce7ddad6cfe2 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 5 Aug 2020 08:13:32 +0000 Subject: [PATCH] WIP (broken) add verify --- mockid/api/common.go | 6 ++ mockid/api/verify.go | 87 ++++++++++++++++++++++++ mockid/mockid_test.go | 33 ++++++++++ mockid/route.go | 1 + xkeypairs/jwk.go | 70 ++++++++++++++++++++ xkeypairs/verify.go | 149 ++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 346 insertions(+) create mode 100644 mockid/api/verify.go create mode 100644 xkeypairs/jwk.go create mode 100644 xkeypairs/verify.go diff --git a/mockid/api/common.go b/mockid/api/common.go index c5c5f3c..2d71551 100644 --- a/mockid/api/common.go +++ b/mockid/api/common.go @@ -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) diff --git a/mockid/api/verify.go b/mockid/api/verify.go new file mode 100644 index 0000000..1f89e4b --- /dev/null +++ b/mockid/api/verify.go @@ -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')) +} diff --git a/mockid/mockid_test.go b/mockid/mockid_test.go index bc672db..57f188d 100644 --- a/mockid/mockid_test.go +++ b/mockid/mockid_test.go @@ -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") diff --git a/mockid/route.go b/mockid/route.go index d627344..437393a 100644 --- a/mockid/route.go +++ b/mockid/route.go @@ -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") diff --git a/xkeypairs/jwk.go b/xkeypairs/jwk.go new file mode 100644 index 0000000..e56a40d --- /dev/null +++ b/xkeypairs/jwk.go @@ -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()), + } +} diff --git a/xkeypairs/verify.go b/xkeypairs/verify.go new file mode 100644 index 0000000..28f9f68 --- /dev/null +++ b/xkeypairs/verify.go @@ -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)) +}