WIP (broken) add verify
This commit is contained in:
parent
a1b4ad1202
commit
e8c50dee76
|
@ -33,6 +33,12 @@ func (o *options) nextReader() io.Reader {
|
||||||
return rand.New(rand.NewSource(o.Seed))
|
return rand.New(rand.NewSource(o.Seed))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
func getJWS(r *http.Request) (*options, error) {
|
||||||
|
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
func getOpts(r *http.Request) (*options, error) {
|
func getOpts(r *http.Request) (*options, error) {
|
||||||
tok := make(map[string]interface{})
|
tok := make(map[string]interface{})
|
||||||
decoder := json.NewDecoder(r.Body)
|
decoder := json.NewDecoder(r.Body)
|
||||||
|
|
|
@ -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'))
|
||||||
|
}
|
|
@ -64,6 +64,39 @@ func TestMain(m *testing.M) {
|
||||||
//func TestSelfSignWithoutExp(t *testing.T)
|
//func TestSelfSignWithoutExp(t *testing.T)
|
||||||
//func TestSelfSignWithJTIWithoutExp(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) {
|
func TestSelfSign(t *testing.T) {
|
||||||
client := srv.Client()
|
client := srv.Client()
|
||||||
//urlstr, _ := url.Parse(srv.URL + "/jose.jws.json")
|
//urlstr, _ := url.Parse(srv.URL + "/jose.jws.json")
|
||||||
|
|
|
@ -192,6 +192,7 @@ func Route(jwksPrefix string, privkey keypairs.PrivateKey) http.Handler {
|
||||||
|
|
||||||
http.HandleFunc("/jose.jws.json", api.SignJWS)
|
http.HandleFunc("/jose.jws.json", api.SignJWS)
|
||||||
http.HandleFunc("/jose.jws.jwt", api.SignJWT)
|
http.HandleFunc("/jose.jws.jwt", api.SignJWT)
|
||||||
|
http.HandleFunc("/verify", api.Verify)
|
||||||
|
|
||||||
http.HandleFunc("/inspect_token", func(w http.ResponseWriter, r *http.Request) {
|
http.HandleFunc("/inspect_token", func(w http.ResponseWriter, r *http.Request) {
|
||||||
token := r.Header.Get("Authorization")
|
token := r.Header.Get("Authorization")
|
||||||
|
|
|
@ -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()),
|
||||||
|
}
|
||||||
|
}
|
|
@ -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))
|
||||||
|
}
|
Loading…
Reference in New Issue