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))
|
||||
}
|
||||
|
||||
/*
|
||||
func getJWS(r *http.Request) (*options, error) {
|
||||
|
||||
}
|
||||
*/
|
||||
|
||||
func getOpts(r *http.Request) (*options, error) {
|
||||
tok := make(map[string]interface{})
|
||||
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 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")
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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