can now self-sign JWS and JWT

This commit is contained in:
AJ ONeal 2020-08-04 07:09:43 +00:00
parent 9b250c8cbb
commit a1b4ad1202
7 changed files with 334 additions and 23 deletions

View File

@ -13,12 +13,16 @@ import (
"net/http" "net/http"
) )
type Object = map[string]interface{}
// options are the things that we may need to know about a request to fulfill it properly // options are the things that we may need to know about a request to fulfill it properly
type options struct { type options struct {
Key string `json:"key"` Key string `json:"key"`
KeyType string `json:"kty"` KeyType string `json:"kty"`
Seed int64 `json:"-"` Seed int64 `json:"-"`
SeedStr string `json:"seed"` SeedStr string `json:"seed"`
Claims Object `json:"claims"`
Header Object `json:"header"`
} }
// this shananigans is only for testing and debug API stuff // this shananigans is only for testing and debug API stuff
@ -55,6 +59,9 @@ func getOpts(r *http.Request) (*options, error) {
Key: key, Key: key,
} }
opts.Claims, _ = tok["claims"].(Object)
opts.Header, _ = tok["header"].(Object)
var n int var n int
if 0 != seed { if 0 != seed {
n = opts.nextReader().(*mathrand.Rand).Intn(2) n = opts.nextReader().(*mathrand.Rand).Intn(2)

57
mockid/api/sign.go Normal file
View File

@ -0,0 +1,57 @@
package api
import (
"encoding/json"
"net/http"
"git.coolaj86.com/coolaj86/go-mockid/xkeypairs"
)
// SignJWS will create an uncompressed JWT with the given payload
func SignJWS(w http.ResponseWriter, r *http.Request) {
sign(w, r, false)
}
// SignJWT will create an compressed JWS (JWT) with the given payload
func SignJWT(w http.ResponseWriter, r *http.Request) {
sign(w, r, true)
}
func sign(w http.ResponseWriter, r *http.Request, jwt bool) {
if "POST" != r.Method {
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
return
}
opts, err := getOpts(r)
if nil != err {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
privkey, err := getPrivKey(opts)
if nil != err {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
header := opts.Header
if 0 != opts.Seed {
header["_seed"] = opts.Seed
}
jws, err := xkeypairs.SignClaims(privkey, header, opts.Claims)
if nil != err {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
var b []byte
if jwt {
s := xkeypairs.JWSToJWT(jws)
w.Write(append([]byte(s), '\n'))
return
}
b, _ = json.Marshal(jws)
w.Write(append(b, '\n'))
}

View File

@ -115,6 +115,29 @@ func GenToken(host string, privkey keypairs.PrivateKey, query url.Values) (strin
return protected, payload, token return protected, payload, token
} }
func JOSESign(privkey keypairs.PrivateKey, hash []byte) []byte {
var sig []byte
switch k := privkey.(type) {
case *rsa.PrivateKey:
panic("TODO: implement rsa sign")
case *ecdsa.PrivateKey:
r, s, _ := ecdsa.Sign(rndsrc, k, hash[:])
rb := r.Bytes()
fmt.Println("debug:")
fmt.Println(r, s)
for len(rb) < 32 {
rb = append([]byte{0}, rb...)
}
sb := s.Bytes()
for len(rb) < 32 {
sb = append([]byte{0}, sb...)
}
sig = append(rb, sb...)
}
return sig
}
// TODO: move to keypairs // TODO: move to keypairs
func JOSEVerify(pubkey keypairs.PublicKey, hash []byte, sig []byte) bool { func JOSEVerify(pubkey keypairs.PublicKey, hash []byte, sig []byte) bool {
@ -143,29 +166,6 @@ func JOSEVerify(pubkey keypairs.PublicKey, hash []byte, sig []byte) bool {
return verified return verified
} }
func JOSESign(privkey keypairs.PrivateKey, hash []byte) []byte {
var sig []byte
switch k := privkey.(type) {
case *rsa.PrivateKey:
panic("TODO: implement rsa sign")
case *ecdsa.PrivateKey:
r, s, _ := ecdsa.Sign(rndsrc, k, hash[:])
rb := r.Bytes()
fmt.Println("debug:")
fmt.Println(r, s)
for len(rb) < 32 {
rb = append([]byte{0}, rb...)
}
sb := s.Bytes()
for len(rb) < 32 {
sb = append([]byte{0}, sb...)
}
sig = append(rb, sb...)
}
return sig
}
func issueNonce(w http.ResponseWriter, r *http.Request) { func issueNonce(w http.ResponseWriter, r *http.Request) {
b := make([]byte, 16) b := make([]byte, 16)
_, _ = rand.Read(b) _, _ = rand.Read(b)

View File

@ -61,6 +61,40 @@ func TestMain(m *testing.M) {
os.Exit(m.Run()) os.Exit(m.Run())
} }
//func TestSelfSignWithoutExp(t *testing.T)
//func TestSelfSignWithJTIWithoutExp(t *testing.T)
func TestSelfSign(t *testing.T) {
client := srv.Client()
//urlstr, _ := url.Parse(srv.URL + "/jose.jws.json")
urlstr, _ := url.Parse(srv.URL + "/jose.jws.jwt")
//fmt.Println("URL:", srv.URL, urlstr)
tokenRequest := []byte(`{"seed":"test","header":{"_jwk":true},"claims":{"sub":"bananas","exp":"10m"}}`)
res, err := client.Do(&http.Request{
Method: "POST",
URL: urlstr,
Body: ioutil.NopCloser(bytes.NewReader(tokenRequest)),
})
if nil != err {
t.Error(err)
return
}
if 200 != res.StatusCode {
t.Error(fmt.Errorf("bad status code: %d", res.StatusCode))
return
}
data, err := ioutil.ReadAll(res.Body)
if nil != err {
t.Error(err)
return
}
log.Printf("TODO: verify, and verify non-self-signed")
log.Printf(string(data))
}
func TestGenerateJWK(t *testing.T) { func TestGenerateJWK(t *testing.T) {
client := srv.Client() client := srv.Client()
urlstr, _ := url.Parse(srv.URL + "/private.jwk.json") urlstr, _ := url.Parse(srv.URL + "/private.jwk.json")
@ -74,6 +108,10 @@ func TestGenerateJWK(t *testing.T) {
t.Error(err) t.Error(err)
return return
} }
if 200 != res.StatusCode {
t.Error(fmt.Errorf("bad status code: %d", res.StatusCode))
return
}
data, err := ioutil.ReadAll(res.Body) data, err := ioutil.ReadAll(res.Body)
if nil != err { if nil != err {
@ -128,6 +166,11 @@ func TestGenWithSeed(t *testing.T) {
t.Error(err) t.Error(err)
return return
} }
if 200 != res.StatusCode {
t.Error(fmt.Errorf("bad status code: %d", res.StatusCode))
return
}
dataA, err := ioutil.ReadAll(res.Body) dataA, err := ioutil.ReadAll(res.Body)
if nil != err { if nil != err {
//t.Fatal(err) //t.Fatal(err)
@ -150,6 +193,11 @@ func TestGenWithSeed(t *testing.T) {
t.Error(err) t.Error(err)
return return
} }
if 200 != res.StatusCode {
t.Error(fmt.Errorf("bad status code: %d", res.StatusCode))
return
}
dataB, err := ioutil.ReadAll(res.Body) dataB, err := ioutil.ReadAll(res.Body)
if nil != err { if nil != err {
//t.Fatal(err) //t.Fatal(err)
@ -180,6 +228,11 @@ func TestGenWithRand(t *testing.T) {
t.Error(err) t.Error(err)
return return
} }
if 200 != res.StatusCode {
t.Error(fmt.Errorf("bad status code: %d", res.StatusCode))
return
}
dataA, err := ioutil.ReadAll(res.Body) dataA, err := ioutil.ReadAll(res.Body)
if nil != err { if nil != err {
//t.Fatal(err) //t.Fatal(err)
@ -200,6 +253,11 @@ func TestGenWithRand(t *testing.T) {
t.Error(err) t.Error(err)
return return
} }
if 200 != res.StatusCode {
t.Error(fmt.Errorf("bad status code: %d", res.StatusCode))
return
}
dataB, err := ioutil.ReadAll(res.Body) dataB, err := ioutil.ReadAll(res.Body)
if nil != err { if nil != err {
//t.Fatal(err) //t.Fatal(err)
@ -226,6 +284,10 @@ func TestGeneratePEM(t *testing.T) {
t.Error(err) t.Error(err)
return return
} }
if 200 != res.StatusCode {
t.Error(fmt.Errorf("bad status code: %d", res.StatusCode))
return
}
data, err := ioutil.ReadAll(res.Body) data, err := ioutil.ReadAll(res.Body)
if nil != err { if nil != err {
@ -266,6 +328,10 @@ func TestPublicJWKWithKey(t *testing.T) {
t.Error(err) t.Error(err)
return return
} }
if 200 != res.StatusCode {
t.Error(fmt.Errorf("bad status code: %d", res.StatusCode))
return
}
data, err := ioutil.ReadAll(res.Body) data, err := ioutil.ReadAll(res.Body)
if nil != err { if nil != err {
@ -319,6 +385,10 @@ func TestPublicPEMWithSeed(t *testing.T) {
t.Error(err) t.Error(err)
return return
} }
if 200 != res.StatusCode {
t.Error(fmt.Errorf("bad status code: %d", res.StatusCode))
return
}
data, err := ioutil.ReadAll(res.Body) data, err := ioutil.ReadAll(res.Body)
if nil != err { if nil != err {

View File

@ -181,6 +181,7 @@ func Route(jwksPrefix string, privkey keypairs.PrivateKey) http.Handler {
fmt.Fprintf(w, token) fmt.Fprintf(w, token)
}) })
// TODO add /debug prefix
http.HandleFunc("/private.jwk.json", api.GeneratePrivateJWK) http.HandleFunc("/private.jwk.json", api.GeneratePrivateJWK)
http.HandleFunc("/priv.der", api.GeneratePrivateDER) http.HandleFunc("/priv.der", api.GeneratePrivateDER)
http.HandleFunc("/priv.pem", api.GeneratePrivatePEM) http.HandleFunc("/priv.pem", api.GeneratePrivatePEM)
@ -189,6 +190,9 @@ func Route(jwksPrefix string, privkey keypairs.PrivateKey) http.Handler {
http.HandleFunc("/pub.der", api.GeneratePublicDER) http.HandleFunc("/pub.der", api.GeneratePublicDER)
http.HandleFunc("/pub.pem", api.GeneratePublicPEM) http.HandleFunc("/pub.pem", api.GeneratePublicPEM)
http.HandleFunc("/jose.jws.json", api.SignJWS)
http.HandleFunc("/jose.jws.jwt", api.SignJWT)
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")
log.Printf("%s %s %s\n", r.Method, r.URL.Path, token) log.Printf("%s %s %s\n", r.Method, r.URL.Path, token)

View File

@ -16,6 +16,7 @@ func ParseDuration(exp string) (int, error) {
if "" == exp { if "" == exp {
exp = "15m" exp = "15m"
} }
mult := 1 mult := 1
switch exp[len(exp)-1] { switch exp[len(exp)-1] {
case 'w': case 'w':
@ -37,9 +38,11 @@ func ParseDuration(exp string) (int, error) {
exp += "s" exp += "s"
} }
// 15m => num=15, mult=1*60
num, err := strconv.Atoi(exp[:len(exp)-1]) num, err := strconv.Atoi(exp[:len(exp)-1])
if nil != err { if nil != err {
return 0, err return 0, err
} }
return num * mult, nil return num * mult, nil
} }

170
xkeypairs/sign.go Normal file
View File

@ -0,0 +1,170 @@
package xkeypairs
import (
"crypto"
"crypto/ecdsa"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
mathrand "math/rand"
"time"
"git.rootprojects.org/root/keypairs"
)
var RandomReader = rand.Reader
type JWS struct {
Header Object `json:"header"` // JSON
Claims Object `json:"claims"` // JSON
Protected string `json:"protected"` // base64
Payload string `json:"payload"` // base64
Signature string `json:"signature"` // base64
}
type Object = map[string]interface{}
func SignClaims(privkey keypairs.PrivateKey, header Object, claims Object) (*JWS, error) {
var randsrc io.Reader = RandomReader
seed, _ := header["_seed"].(int64)
if 0 != seed {
randsrc = mathrand.New(mathrand.NewSource(seed))
//delete(header, "_seed")
}
protected, err := headerToProtected(keypairs.NewPublicKey(privkey.Public()), header)
if nil != err {
return nil, err
}
protected64 := base64.RawURLEncoding.EncodeToString(protected)
payload, err := claimsToPayload(claims)
if nil != err {
return nil, err
}
payload64 := base64.RawURLEncoding.EncodeToString(payload)
hash := sha256.Sum256([]byte(fmt.Sprintf(
`%s.%s`,
protected64,
payload64,
)))
sig := Sign(randsrc, privkey, hash[:])
sig64 := base64.RawURLEncoding.EncodeToString(sig)
return &JWS{
Header: header,
Claims: claims,
Protected: protected64,
Payload: payload64,
Signature: sig64,
}, nil
}
func headerToProtected(pub keypairs.PublicKey, header Object) ([]byte, error) {
if nil == header {
header = Object{}
}
// Only supporting 2048-bit and P256 keys right now
// because that's all that's practical and well-supported.
// No security theatre here.
alg := "ES256"
switch pub.Key().(type) {
case *rsa.PublicKey:
alg = "RS256"
}
if selfSign, _ := header["_jwk"].(bool); selfSign {
delete(header, "_jwk")
any := Object{}
_ = json.Unmarshal(keypairs.MarshalJWKPublicKey(pub), &any)
header["jwk"] = any
}
// TODO what are the acceptable values? JWT. JWS? others?
header["typ"] = "JWT"
if _, ok := header["jwk"]; !ok {
thumbprint := keypairs.ThumbprintPublicKey(pub)
kid, _ := header["kid"].(string)
if "" != kid && thumbprint != kid {
return nil, errors.New("'kid' should be the key's thumbprint")
}
header["kid"] = thumbprint
}
header["alg"] = alg
protected, err := json.Marshal(header)
if nil != err {
return nil, err
}
return protected, nil
}
func claimsToPayload(claims Object) ([]byte, error) {
if nil == claims {
claims = Object{}
}
jti, _ := claims["jti"].(string)
exp, _ := claims["exp"].(int64)
dur, _ := claims["exp"].(string)
insecure, _ := claims["insecure"].(bool)
// parse if exp is actually a duration, such as "15m"
if 0 == exp && "" != dur {
s, err := ParseDuration(dur)
if nil != err {
return nil, err
}
exp = time.Now().Add(time.Duration(s) * time.Second).Unix()
claims["exp"] = exp
}
if "" == jti && 0 == exp && !insecure {
return nil, errors.New("token must have jti or exp as to be expirable / cancellable")
}
return json.Marshal(claims)
}
func JWSToJWT(jwt *JWS) string {
return fmt.Sprintf(
"%s.%s.%s",
jwt.Protected,
jwt.Payload,
jwt.Signature,
)
}
func Sign(rand io.Reader, privkey keypairs.PrivateKey, hash []byte) []byte {
var sig []byte
if len(hash) != 32 {
panic("only 256-bit hashes for 2048-bit and 256-bit keys are supported")
}
switch k := privkey.(type) {
case *rsa.PrivateKey:
sig, _ = rsa.SignPKCS1v15(rand, k, crypto.SHA256, hash)
case *ecdsa.PrivateKey:
r, s, _ := ecdsa.Sign(rand, k, hash[:])
rb := r.Bytes()
fmt.Println("debug:")
fmt.Println(r, s)
for len(rb) < 32 {
rb = append([]byte{0}, rb...)
}
sb := s.Bytes()
for len(rb) < 32 {
sb = append([]byte{0}, sb...)
}
sig = append(rb, sb...)
}
return sig
}