| 
									
										
										
										
											2020-08-04 07:09:43 +00:00
										 |  |  | 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) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-05 09:06:45 +00:00
										 |  |  | 	signable := fmt.Sprintf(`%s.%s`, protected64, payload64) | 
					
						
							|  |  |  | 	hash := sha256.Sum256([]byte(signable)) | 
					
						
							| 
									
										
										
										
											2020-08-04 07:09:43 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	sig := Sign(randsrc, privkey, hash[:]) | 
					
						
							|  |  |  | 	sig64 := base64.RawURLEncoding.EncodeToString(sig) | 
					
						
							| 
									
										
										
										
											2020-08-05 09:06:45 +00:00
										 |  |  | 	//log.Printf("\n(Sign)\nSignable: %s", signable) | 
					
						
							|  |  |  | 	//log.Printf("Hash: %s", hash) | 
					
						
							|  |  |  | 	//log.Printf("Sig: %s", sig64) | 
					
						
							| 
									
										
										
										
											2020-08-04 07:09:43 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	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 | 
					
						
							|  |  |  | } |