can now self-sign JWS and JWT
This commit is contained in:
		
							부모
							
								
									9b250c8cbb
								
							
						
					
					
						커밋
						a1b4ad1202
					
				| @ -13,12 +13,16 @@ import ( | ||||
| 	"net/http" | ||||
| ) | ||||
| 
 | ||||
| type Object = map[string]interface{} | ||||
| 
 | ||||
| // options are the things that we may need to know about a request to fulfill it properly | ||||
| type options struct { | ||||
| 	Key     string `json:"key"` | ||||
| 	KeyType string `json:"kty"` | ||||
| 	Seed    int64  `json:"-"` | ||||
| 	SeedStr string `json:"seed"` | ||||
| 	Claims  Object `json:"claims"` | ||||
| 	Header  Object `json:"header"` | ||||
| } | ||||
| 
 | ||||
| // this shananigans is only for testing and debug API stuff | ||||
| @ -55,6 +59,9 @@ func getOpts(r *http.Request) (*options, error) { | ||||
| 		Key:  key, | ||||
| 	} | ||||
| 
 | ||||
| 	opts.Claims, _ = tok["claims"].(Object) | ||||
| 	opts.Header, _ = tok["header"].(Object) | ||||
| 
 | ||||
| 	var n int | ||||
| 	if 0 != seed { | ||||
| 		n = opts.nextReader().(*mathrand.Rand).Intn(2) | ||||
|  | ||||
							
								
								
									
										57
									
								
								mockid/api/sign.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								mockid/api/sign.go
									
									
									
									
									
										Normal 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')) | ||||
| } | ||||
| @ -115,6 +115,29 @@ func GenToken(host string, privkey keypairs.PrivateKey, query url.Values) (strin | ||||
| 	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 | ||||
| 
 | ||||
| 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 | ||||
| } | ||||
| 
 | ||||
| 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) { | ||||
| 	b := make([]byte, 16) | ||||
| 	_, _ = rand.Read(b) | ||||
|  | ||||
| @ -61,6 +61,40 @@ func TestMain(m *testing.M) { | ||||
| 	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) { | ||||
| 	client := srv.Client() | ||||
| 	urlstr, _ := url.Parse(srv.URL + "/private.jwk.json") | ||||
| @ -74,6 +108,10 @@ func TestGenerateJWK(t *testing.T) { | ||||
| 		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 { | ||||
| @ -128,6 +166,11 @@ func TestGenWithSeed(t *testing.T) { | ||||
| 		t.Error(err) | ||||
| 		return | ||||
| 	} | ||||
| 	if 200 != res.StatusCode { | ||||
| 		t.Error(fmt.Errorf("bad status code: %d", res.StatusCode)) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	dataA, err := ioutil.ReadAll(res.Body) | ||||
| 	if nil != err { | ||||
| 		//t.Fatal(err) | ||||
| @ -150,6 +193,11 @@ func TestGenWithSeed(t *testing.T) { | ||||
| 			t.Error(err) | ||||
| 			return | ||||
| 		} | ||||
| 		if 200 != res.StatusCode { | ||||
| 			t.Error(fmt.Errorf("bad status code: %d", res.StatusCode)) | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		dataB, err := ioutil.ReadAll(res.Body) | ||||
| 		if nil != err { | ||||
| 			//t.Fatal(err) | ||||
| @ -180,6 +228,11 @@ func TestGenWithRand(t *testing.T) { | ||||
| 		t.Error(err) | ||||
| 		return | ||||
| 	} | ||||
| 	if 200 != res.StatusCode { | ||||
| 		t.Error(fmt.Errorf("bad status code: %d", res.StatusCode)) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	dataA, err := ioutil.ReadAll(res.Body) | ||||
| 	if nil != err { | ||||
| 		//t.Fatal(err) | ||||
| @ -200,6 +253,11 @@ func TestGenWithRand(t *testing.T) { | ||||
| 		t.Error(err) | ||||
| 		return | ||||
| 	} | ||||
| 	if 200 != res.StatusCode { | ||||
| 		t.Error(fmt.Errorf("bad status code: %d", res.StatusCode)) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	dataB, err := ioutil.ReadAll(res.Body) | ||||
| 	if nil != err { | ||||
| 		//t.Fatal(err) | ||||
| @ -226,6 +284,10 @@ func TestGeneratePEM(t *testing.T) { | ||||
| 		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 { | ||||
| @ -266,6 +328,10 @@ func TestPublicJWKWithKey(t *testing.T) { | ||||
| 		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 { | ||||
| @ -319,6 +385,10 @@ func TestPublicPEMWithSeed(t *testing.T) { | ||||
| 		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 { | ||||
|  | ||||
| @ -181,6 +181,7 @@ func Route(jwksPrefix string, privkey keypairs.PrivateKey) http.Handler { | ||||
| 		fmt.Fprintf(w, token) | ||||
| 	}) | ||||
| 
 | ||||
| 	// TODO add /debug prefix | ||||
| 	http.HandleFunc("/private.jwk.json", api.GeneratePrivateJWK) | ||||
| 	http.HandleFunc("/priv.der", api.GeneratePrivateDER) | ||||
| 	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.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) { | ||||
| 		token := r.Header.Get("Authorization") | ||||
| 		log.Printf("%s %s %s\n", r.Method, r.URL.Path, token) | ||||
|  | ||||
| @ -16,6 +16,7 @@ func ParseDuration(exp string) (int, error) { | ||||
| 	if "" == exp { | ||||
| 		exp = "15m" | ||||
| 	} | ||||
| 
 | ||||
| 	mult := 1 | ||||
| 	switch exp[len(exp)-1] { | ||||
| 	case 'w': | ||||
| @ -37,9 +38,11 @@ func ParseDuration(exp string) (int, error) { | ||||
| 		exp += "s" | ||||
| 	} | ||||
| 
 | ||||
| 	// 15m => num=15, mult=1*60 | ||||
| 	num, err := strconv.Atoi(exp[:len(exp)-1]) | ||||
| 	if nil != err { | ||||
| 		return 0, err | ||||
| 	} | ||||
| 
 | ||||
| 	return num * mult, nil | ||||
| } | ||||
|  | ||||
							
								
								
									
										170
									
								
								xkeypairs/sign.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										170
									
								
								xkeypairs/sign.go
									
									
									
									
									
										Normal 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 | ||||
| } | ||||
		불러오는 중...
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user