mirror of
				https://github.com/therootcompany/keypairs
				synced 2025-10-26 18:52:50 +00:00 
			
		
		
		
	add verify subcommand
This commit is contained in:
		
							parent
							
								
									a560441c61
								
							
						
					
					
						commit
						d42763516f
					
				
							
								
								
									
										19
									
								
								cli_test.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								cli_test.sh
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,19 @@ | |||||||
|  | #!/bin/bash | ||||||
|  | set -u | ||||||
|  | 
 | ||||||
|  | go build -mod=vendor cmd/keypairs/*.go | ||||||
|  | ./keypairs gen > testkey.jwk.json 2> testpub.jwk.json | ||||||
|  | 
 | ||||||
|  | ./keypairs sign --exp 1h ./testkey.jwk.json '{"foo":"bar"}' > testjwt.txt 2> testjws.json | ||||||
|  | 
 | ||||||
|  | echo "" | ||||||
|  | echo "Should pass:" | ||||||
|  | ./keypairs verify ./testpub.jwk.json testjwt.txt > /dev/null | ||||||
|  | ./keypairs verify ./testpub.jwk.json "$(cat testjwt.txt)" > /dev/null | ||||||
|  | ./keypairs verify ./testpub.jwk.json testjws.json > /dev/null | ||||||
|  | ./keypairs verify ./testpub.jwk.json "$(cat testjws.json)" > /dev/null | ||||||
|  | 
 | ||||||
|  | echo "" | ||||||
|  | echo "Should fail:" | ||||||
|  | ./keypairs sign --exp -1m ./testkey.jwk.json '{"bar":"foo"}' > errjwt.txt 2> errjws.json | ||||||
|  | ./keypairs verify ./testpub.jwk.json errjwt.txt > /dev/null | ||||||
| @ -30,13 +30,17 @@ func usage() { | |||||||
| 	fmt.Println("    version") | 	fmt.Println("    version") | ||||||
| 	fmt.Println("    gen") | 	fmt.Println("    gen") | ||||||
| 	fmt.Println("    sign") | 	fmt.Println("    sign") | ||||||
|  | 	fmt.Println("    verify") | ||||||
| 	fmt.Println("") | 	fmt.Println("") | ||||||
| 	fmt.Println("Examples:") | 	fmt.Println("Examples:") | ||||||
| 	fmt.Println("    keypairs gen -o key.jwk.json [--pub <public-key>]") | 	fmt.Println("    keypairs gen -o key.jwk.json [--pub <public-key>]") | ||||||
|  | 	fmt.Println("") | ||||||
| 	fmt.Println("    keypairs sign --exp 15m key.jwk.json payload.json") | 	fmt.Println("    keypairs sign --exp 15m key.jwk.json payload.json") | ||||||
| 	fmt.Println("    keypairs sign --exp 15m key.jwk.json '{ \"sub\": \"xxxx\" }'") | 	fmt.Println("    keypairs sign --exp 15m key.jwk.json '{ \"sub\": \"xxxx\" }'") | ||||||
| 	fmt.Println("") | 	fmt.Println("") | ||||||
| 	//fmt.Println("  verify") | 	fmt.Println("    keypairs verify ./pub.jwk.json 'xxxx.yyyy.zzzz'") | ||||||
|  | 	// TODO fmt.Println("    keypairs verify --issuer https://example.com '{ \"sub\": \"xxxx\" }'") | ||||||
|  | 	fmt.Println("") | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func ver() { | func ver() { | ||||||
| @ -65,9 +69,11 @@ func main() { | |||||||
| 		os.Exit(0) | 		os.Exit(0) | ||||||
| 		return | 		return | ||||||
| 	case "gen": | 	case "gen": | ||||||
| 		gen(args) | 		gen(args[2:]) | ||||||
| 	case "sign": | 	case "sign": | ||||||
| 		sign(args) | 		sign(args[2:]) | ||||||
|  | 	case "verify": | ||||||
|  | 		verify(args[2:]) | ||||||
| 	default: | 	default: | ||||||
| 		usage() | 		usage() | ||||||
| 		os.Exit(1) | 		os.Exit(1) | ||||||
| @ -94,42 +100,27 @@ func sign(args []string) { | |||||||
| 	flags := flag.NewFlagSet("sign", flag.ExitOnError) | 	flags := flag.NewFlagSet("sign", flag.ExitOnError) | ||||||
| 	flags.DurationVar(&exp, "exp", 0, "duration until token expires (Default 15m)") | 	flags.DurationVar(&exp, "exp", 0, "duration until token expires (Default 15m)") | ||||||
| 	flags.Parse(args) | 	flags.Parse(args) | ||||||
| 	if len(flags.Args()) <= 3 { | 	if len(flags.Args()) <= 1 { | ||||||
| 		fmt.Fprintf(os.Stderr, "Usage: keypairs sign --exp 1h <private PEM or JWK> ./payload.json\n") | 		fmt.Fprintf(os.Stderr, "Usage: keypairs sign --exp 1h <private PEM or JWK> ./payload.json\n") | ||||||
| 		os.Exit(1) | 		os.Exit(1) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	keyname := flags.Args()[2] | 	keyname := flags.Args()[0] | ||||||
| 	payload := flags.Args()[3] | 	payload := flags.Args()[1] | ||||||
| 
 | 
 | ||||||
| 	var key keypairs.PrivateKey = nil | 	key, err := readKey(keyname) | ||||||
| 	b, err := ioutil.ReadFile(keyname) |  | ||||||
| 	if nil != err { | 	if nil != err { | ||||||
| 		var err2 error | 		fmt.Fprintf(os.Stderr, "%v\n", err) | ||||||
| 		key, err2 = keypairs.ParsePrivateKey([]byte(keyname)) |  | ||||||
| 		if nil != err2 { |  | ||||||
| 			fmt.Fprintf(os.Stderr, |  | ||||||
| 				"could not read private key as file (or parse as string) %q: %s\n", keyname, err) |  | ||||||
| 		} |  | ||||||
| 		os.Exit(1) | 		os.Exit(1) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	if nil == key { |  | ||||||
| 		var err3 error |  | ||||||
| 		key, err3 = keypairs.ParsePrivateKey(b) |  | ||||||
| 		if nil != err3 { |  | ||||||
| 			fmt.Fprintf(os.Stderr, |  | ||||||
| 				"could not parse private key from file %q: %s\n", keyname, err3) |  | ||||||
| 			os.Exit(1) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
| 	if "" == payload { | 	if "" == payload { | ||||||
|  | 		// TODO should this be null? I forget | ||||||
| 		payload = "{}" | 		payload = "{}" | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	b, err = ioutil.ReadFile(payload) | 	b, err := ioutil.ReadFile(payload) | ||||||
| 	claims := map[string]interface{}{} | 	claims := map[string]interface{}{} | ||||||
| 	if nil != err { | 	if nil != err { | ||||||
| 		var err2 error | 		var err2 error | ||||||
| @ -167,8 +158,159 @@ func sign(args []string) { | |||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	b, _ = json.Marshal(&jws) | 	b, _ = json.Marshal(&jws) | ||||||
| 	fmt.Printf("JWS:\n%s\n\n", indentJSON(b)) | 	fmt.Fprintf(os.Stderr, "%s\n", indentJSON(b)) | ||||||
| 	fmt.Printf("JWT:\n%s\n\n", keypairs.JWSToJWT(jws)) | 	fmt.Fprintf(os.Stdout, "%s\n", keypairs.JWSToJWT(jws)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func verify(args []string) { | ||||||
|  | 	flags := flag.NewFlagSet("verify", flag.ExitOnError) | ||||||
|  | 	flags.Usage = func() { | ||||||
|  | 		fmt.Println("Usage: keypairs verify <public key> <jwt-or-jwt>") | ||||||
|  | 		fmt.Println("") | ||||||
|  | 		fmt.Println("    <public key>: a File or String of an EC or RSA key in JWK or PEM format") | ||||||
|  | 		fmt.Println("    <jwt-or-jws>: a JWT or JWS File or String, if JWS the payload must be Base64") | ||||||
|  | 		fmt.Println("") | ||||||
|  | 	} | ||||||
|  | 	flags.Parse(args) | ||||||
|  | 	if len(flags.Args()) <= 1 { | ||||||
|  | 		flags.Usage() | ||||||
|  | 		os.Exit(1) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	pubname := flags.Args()[0] | ||||||
|  | 	payload := flags.Args()[1] | ||||||
|  | 
 | ||||||
|  | 	pub, err := readPub(pubname) | ||||||
|  | 	if nil != err { | ||||||
|  | 		fmt.Fprintf(os.Stderr, "%v\n", err) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	jws, err := readJWS(payload) | ||||||
|  | 	if nil != err { | ||||||
|  | 		fmt.Fprintf(os.Stderr, "%v\n", err) | ||||||
|  | 		os.Exit(1) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	b, _ := json.Marshal(&jws) | ||||||
|  | 	fmt.Fprintf(os.Stdout, "%s\n", indentJSON(b)) | ||||||
|  | 
 | ||||||
|  | 	errs := keypairs.VerifyClaims(pub, jws) | ||||||
|  | 	if nil != errs { | ||||||
|  | 		fmt.Fprintf(os.Stderr, "error:\n") | ||||||
|  | 		for _, err := range errs { | ||||||
|  | 			fmt.Fprintf(os.Stderr, "\t%v\n", err) | ||||||
|  | 		} | ||||||
|  | 		os.Exit(1) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	fmt.Fprintf(os.Stderr, "Signature is Valid\n") | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func readKey(keyname string) (keypairs.PrivateKey, error) { | ||||||
|  | 	var key keypairs.PrivateKey = nil | ||||||
|  | 
 | ||||||
|  | 	// Read as file | ||||||
|  | 	b, err := ioutil.ReadFile(keyname) | ||||||
|  | 	if nil != err { | ||||||
|  | 		// Tis not a file! Perhaps a string? | ||||||
|  | 		var err2 error | ||||||
|  | 		key, err2 = keypairs.ParsePrivateKey([]byte(keyname)) | ||||||
|  | 		if nil != err2 { | ||||||
|  | 			// Neither a valid string. Blast! | ||||||
|  | 			return nil, fmt.Errorf( | ||||||
|  | 				"could not read private key as file (or parse as string) %q:\n%s", | ||||||
|  | 				keyname, err2, | ||||||
|  | 			) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if nil == key { | ||||||
|  | 		var err3 error | ||||||
|  | 		key, err3 = keypairs.ParsePrivateKey(b) | ||||||
|  | 		if nil != err3 { | ||||||
|  | 			return nil, fmt.Errorf( | ||||||
|  | 				"could not parse private key from file %q:\n%s", | ||||||
|  | 				keyname, err3, | ||||||
|  | 			) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return key, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func readPub(pubname string) (keypairs.PublicKey, error) { | ||||||
|  | 	var pub keypairs.PublicKey = nil | ||||||
|  | 
 | ||||||
|  | 	// Read as file | ||||||
|  | 	b, err := ioutil.ReadFile(pubname) | ||||||
|  | 	if nil != err { | ||||||
|  | 		// No file? Try as string! | ||||||
|  | 		var err2 error | ||||||
|  | 		pub, err2 = keypairs.ParsePublicKey([]byte(pubname)) | ||||||
|  | 		if nil != err2 { | ||||||
|  | 			return nil, fmt.Errorf( | ||||||
|  | 				"could not read public key as file (or parse as string) %q:\n%w", | ||||||
|  | 				pubname, err, | ||||||
|  | 			) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Oh, it was a file. | ||||||
|  | 	if nil == pub { | ||||||
|  | 		var err3 error | ||||||
|  | 		pub, err3 = keypairs.ParsePublicKey(b) | ||||||
|  | 		if nil != err3 { | ||||||
|  | 			return nil, fmt.Errorf( | ||||||
|  | 				"could not parse public key from file %q:\n%w", | ||||||
|  | 				pubname, err3, | ||||||
|  | 			) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return pub, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func readJWS(payload string) (*keypairs.JWS, error) { | ||||||
|  | 	// Is it a file? | ||||||
|  | 	b, err := ioutil.ReadFile(payload) | ||||||
|  | 	if nil != err { | ||||||
|  | 		// Or a JWS or JWS String!? | ||||||
|  | 		b = []byte(payload) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Either way, we have some bytes now | ||||||
|  | 	jws := &keypairs.JWS{} | ||||||
|  | 	jwt := string(b) | ||||||
|  | 	jwsb := []byte(jwt) | ||||||
|  | 	if !strings.Contains(jwt, " \t\n{}[]") { | ||||||
|  | 		jws = keypairs.JWTToJWS(string(b)) | ||||||
|  | 		if nil != jws { | ||||||
|  | 			b, _ = json.Marshal(jws) | ||||||
|  | 			jwsb = (b) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// And now we have a string that may be a JWS | ||||||
|  | 	if err := json.Unmarshal(jwsb, &jws); nil != err { | ||||||
|  | 		// Nope, it's not | ||||||
|  | 		return nil, fmt.Errorf( | ||||||
|  | 			"could not read signed payload from file or string as JWT or JWS %q:\n%w", | ||||||
|  | 			payload, err, | ||||||
|  | 		) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if err := jws.DecodeComponents(); nil != err { | ||||||
|  | 		// bah! so close! | ||||||
|  | 		return nil, fmt.Errorf( | ||||||
|  | 			"could not decode the JWS Header and Claims components: %w\n%s", | ||||||
|  | 			err, string(jwsb), | ||||||
|  | 		) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return jws, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func marshalPriv(key keypairs.PrivateKey, keyname string) { | func marshalPriv(key keypairs.PrivateKey, keyname string) { | ||||||
|  | |||||||
							
								
								
									
										46
									
								
								generate.go
									
									
									
									
									
								
							
							
						
						
									
										46
									
								
								generate.go
									
									
									
									
									
								
							| @ -11,34 +11,36 @@ import ( | |||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| var randReader io.Reader = rand.Reader | var randReader io.Reader = rand.Reader | ||||||
| var maxRetry = 1 | var allowMocking = false | ||||||
| 
 | 
 | ||||||
| // KeyOptions are the things that we may need to know about a request to fulfill it properly | // KeyOptions are the things that we may need to know about a request to fulfill it properly | ||||||
| type keyOptions struct { | type keyOptions struct { | ||||||
| 	//Key     string `json:"key"` | 	//Key     string `json:"key"` | ||||||
| 	KeyType  string `json:"kty"` | 	KeyType  string `json:"kty"` | ||||||
| 	//Seed    int64  `json:"-"` | 	mockSeed int64  //`json:"-"` | ||||||
| 	//SeedStr string `json:"seed"` | 	//SeedStr string `json:"seed"` | ||||||
| 	//Claims  Object `json:"claims"` | 	//Claims  Object `json:"claims"` | ||||||
| 	//Header  Object `json:"header"` | 	//Header  Object `json:"header"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // this shananigans is only for testing and debug API stuff | func (o *keyOptions) nextReader() io.Reader { | ||||||
| func (o *keyOptions) myFooNextReader() io.Reader { | 	if allowMocking { | ||||||
| 	return randReader | 		return o.maybeMockReader() | ||||||
| 	/* |  | ||||||
| 		if 0 == o.Seed { |  | ||||||
| 			return randReader |  | ||||||
| 	} | 	} | ||||||
| 		return mathrand.New(mathrand.NewSource(o.Seed)) | 	return randReader | ||||||
| 	*/ |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // NewDefaultPrivateKey generates a key with reasonable strength. | // NewDefaultPrivateKey generates a key with reasonable strength. | ||||||
| // Today that means a 256-bit equivalent - either RSA 2048 or EC P-256. | // Today that means a 256-bit equivalent - either RSA 2048 or EC P-256. | ||||||
| func NewDefaultPrivateKey() PrivateKey { | func NewDefaultPrivateKey() PrivateKey { | ||||||
|  | 	// insecure random is okay here, | ||||||
|  | 	// it's just used for a coin toss | ||||||
| 	mathrand.Seed(time.Now().UnixNano()) | 	mathrand.Seed(time.Now().UnixNano()) | ||||||
| 	coin := mathrand.Int() | 	coin := mathrand.Int() | ||||||
|  | 
 | ||||||
|  | 	// the idea here is that we want to make | ||||||
|  | 	// it dead simple to support RSA and EC | ||||||
|  | 	// so it shouldn't matter which is used | ||||||
| 	if 0 == coin%2 { | 	if 0 == coin%2 { | ||||||
| 		return newPrivateKey(&keyOptions{ | 		return newPrivateKey(&keyOptions{ | ||||||
| 			KeyType: "RSA", | 			KeyType: "RSA", | ||||||
| @ -55,29 +57,13 @@ func newPrivateKey(opts *keyOptions) PrivateKey { | |||||||
| 
 | 
 | ||||||
| 	if "RSA" == opts.KeyType { | 	if "RSA" == opts.KeyType { | ||||||
| 		keylen := 2048 | 		keylen := 2048 | ||||||
| 		privkey, _ = rsa.GenerateKey(opts.myFooNextReader(), keylen) | 		privkey, _ = rsa.GenerateKey(opts.nextReader(), keylen) | ||||||
| 		/* | 		if allowMocking { | ||||||
| 			if 0 != opts.Seed { | 			privkey = maybeDerandomizeMockKey(privkey, keylen, opts) | ||||||
| 				for i := 0; i < maxRetry; i++ { |  | ||||||
| 					otherkey, _ := rsa.GenerateKey(opts.myFooNextReader(), 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 { | 	} else { | ||||||
| 		// TODO: EC keys may also suffer the same random problems in the future | 		// TODO: EC keys may also suffer the same random problems in the future | ||||||
| 		privkey, _ = ecdsa.GenerateKey(elliptic.P256(), opts.myFooNextReader()) | 		privkey, _ = ecdsa.GenerateKey(elliptic.P256(), opts.nextReader()) | ||||||
| 	} | 	} | ||||||
| 	return privkey | 	return privkey | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										69
									
								
								jwk.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								jwk.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,69 @@ | |||||||
|  | package keypairs | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // JWK abstracts EC and RSA keys | ||||||
|  | type JWK interface { | ||||||
|  | 	marshalJWK() ([]byte, error) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ECJWK is the EC variant | ||||||
|  | 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 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // RSAJWK is the RSA variant | ||||||
|  | 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 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /* | ||||||
|  | // ToPublicJWK exposes only the public parts | ||||||
|  | func ToPublicJWK(pubkey 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()), | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | */ | ||||||
							
								
								
									
										63
									
								
								jws.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								jws.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,63 @@ | |||||||
|  | package keypairs | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"encoding/base64" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"strings" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // JWS is a parsed JWT, representation as signable/verifiable and human-readable parts | ||||||
|  | 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 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // JWSToJWT joins JWS parts into a JWT as {ProtectedHeader}.{SerializedPayload}.{Signature}. | ||||||
|  | func JWSToJWT(jwt *JWS) string { | ||||||
|  | 	return fmt.Sprintf( | ||||||
|  | 		"%s.%s.%s", | ||||||
|  | 		jwt.Protected, | ||||||
|  | 		jwt.Payload, | ||||||
|  | 		jwt.Signature, | ||||||
|  | 	) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // JWTToJWS splits the JWT into its JWS segments | ||||||
|  | func JWTToJWS(jwt string) (jws *JWS) { | ||||||
|  | 	jwt = strings.TrimSpace(jwt) | ||||||
|  | 	parts := strings.Split(jwt, ".") | ||||||
|  | 	if 3 != len(parts) { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 	return &JWS{ | ||||||
|  | 		Protected: parts[0], | ||||||
|  | 		Payload:   parts[1], | ||||||
|  | 		Signature: parts[2], | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // DecodeComponents decodes JWS Header and Claims | ||||||
|  | func (jws *JWS) DecodeComponents() error { | ||||||
|  | 	protected, err := base64.RawURLEncoding.DecodeString(jws.Protected) | ||||||
|  | 	if nil != err { | ||||||
|  | 		return errors.New("invalid JWS header base64Url encoding") | ||||||
|  | 	} | ||||||
|  | 	if err := json.Unmarshal([]byte(protected), &jws.Header); nil != err { | ||||||
|  | 		return errors.New("invalid JWS header") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	payload, err := base64.RawURLEncoding.DecodeString(jws.Payload) | ||||||
|  | 	if nil != err { | ||||||
|  | 		return errors.New("invalid JWS payload base64Url encoding") | ||||||
|  | 	} | ||||||
|  | 	if err := json.Unmarshal([]byte(payload), &jws.Claims); nil != err { | ||||||
|  | 		return errors.New("invalid JWS claims") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
							
								
								
									
										46
									
								
								mock.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								mock.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,46 @@ | |||||||
|  | package keypairs | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"crypto/rsa" | ||||||
|  | 	"io" | ||||||
|  | 	"log" | ||||||
|  | 	mathrand "math/rand" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // this shananigans is only for testing and debug API stuff | ||||||
|  | func (o *keyOptions) maybeMockReader() io.Reader { | ||||||
|  | 	if !allowMocking { | ||||||
|  | 		panic("mock method called when mocking is not allowed") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if 0 == o.mockSeed { | ||||||
|  | 		return randReader | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	log.Println("WARNING: MOCK: using insecure reader") | ||||||
|  | 	return mathrand.New(mathrand.NewSource(o.mockSeed)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const maxRetry = 16 | ||||||
|  | 
 | ||||||
|  | func maybeDerandomizeMockKey(privkey PrivateKey, keylen int, opts *keyOptions) PrivateKey { | ||||||
|  | 	if 0 != opts.mockSeed { | ||||||
|  | 		for i := 0; i < maxRetry; i++ { | ||||||
|  | 			otherkey, _ := rsa.GenerateKey(opts.nextReader(), 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) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return privkey | ||||||
|  | } | ||||||
							
								
								
									
										26
									
								
								sign.go
									
									
									
									
									
								
							
							
						
						
									
										26
									
								
								sign.go
									
									
									
									
									
								
							| @ -10,24 +10,10 @@ import ( | |||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io" | 	"io" | ||||||
| 	mathrand "math/rand" | 	mathrand "math/rand" // to be used for good, not evil | ||||||
| 	"time" | 	"time" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // randReader may be overwritten for testing |  | ||||||
| //var randReader io.Reader = rand.Reader |  | ||||||
| 
 |  | ||||||
| //var randReader = rand.Reader |  | ||||||
| 
 |  | ||||||
| // JWS is a parsed JWT, representation as signable/verifiable and human-readable parts |  | ||||||
| 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 |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Object is a type alias representing generic JSON data | // Object is a type alias representing generic JSON data | ||||||
| type Object = map[string]interface{} | type Object = map[string]interface{} | ||||||
| 
 | 
 | ||||||
| @ -149,16 +135,6 @@ func claimsToPayload(claims Object) ([]byte, error) { | |||||||
| 	return json.Marshal(claims) | 	return json.Marshal(claims) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // JWSToJWT joins JWS parts into a JWT as {ProtectedHeader}.{SerializedPayload}.{Signature}. |  | ||||||
| func JWSToJWT(jwt *JWS) string { |  | ||||||
| 	return fmt.Sprintf( |  | ||||||
| 		"%s.%s.%s", |  | ||||||
| 		jwt.Protected, |  | ||||||
| 		jwt.Payload, |  | ||||||
| 		jwt.Signature, |  | ||||||
| 	) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Sign signs both RSA and ECDSA. Use `nil` or `crypto/rand.Reader` except for debugging. | // Sign signs both RSA and ECDSA. Use `nil` or `crypto/rand.Reader` except for debugging. | ||||||
| func Sign(privkey PrivateKey, hash []byte, rand io.Reader) []byte { | func Sign(privkey PrivateKey, hash []byte, rand io.Reader) []byte { | ||||||
| 	if nil == rand { | 	if nil == rand { | ||||||
|  | |||||||
							
								
								
									
										174
									
								
								verify.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										174
									
								
								verify.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,174 @@ | |||||||
|  | package keypairs | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"crypto" | ||||||
|  | 	"crypto/ecdsa" | ||||||
|  | 	"crypto/rsa" | ||||||
|  | 	"crypto/sha256" | ||||||
|  | 	"crypto/subtle" | ||||||
|  | 	"encoding/base64" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"log" | ||||||
|  | 	"math/big" | ||||||
|  | 	"time" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // VerifyClaims will check the signature of a parsed JWT | ||||||
|  | func VerifyClaims(pubkey PublicKey, jws *JWS) (errs []error) { | ||||||
|  | 	kid, _ := jws.Header["kid"].(string) | ||||||
|  | 	jwkmap, hasJWK := jws.Header["jwk"].(Object) | ||||||
|  | 	//var jwk JWK = nil | ||||||
|  | 
 | ||||||
|  | 	seed, _ := jws.Header["_seed"].(int64) | ||||||
|  | 	seedf64, _ := jws.Header["_seed"].(float64) | ||||||
|  | 	kty, _ := jws.Header["_kty"].(string) | ||||||
|  | 	if 0 == seed { | ||||||
|  | 		seed = int64(seedf64) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	var pub PublicKey = nil | ||||||
|  | 	if hasJWK { | ||||||
|  | 		pub, errs = selfsignCheck(jwkmap, errs) | ||||||
|  | 	} else { | ||||||
|  | 		opts := &keyOptions{mockSeed: seed, KeyType: kty} | ||||||
|  | 		pub, errs = pubkeyCheck(pubkey, kid, opts, errs) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	jti, _ := jws.Claims["jti"].(string) | ||||||
|  | 	expf64, _ := jws.Claims["exp"].(float64) | ||||||
|  | 	exp := int64(expf64) | ||||||
|  | 	if 0 == exp { | ||||||
|  | 		if "" == jti { | ||||||
|  | 			err := errors.New("one of 'jti' or 'exp' must exist for token expiry") | ||||||
|  | 			errs = append(errs, err) | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		if time.Now().Unix() > exp { | ||||||
|  | 			err := fmt.Errorf("token expired at %d (%s)", exp, time.Unix(exp, 0)) | ||||||
|  | 			errs = append(errs, err) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	signable := fmt.Sprintf("%s.%s", jws.Protected, jws.Payload) | ||||||
|  | 	hash := sha256.Sum256([]byte(signable)) | ||||||
|  | 	sig, err := base64.RawURLEncoding.DecodeString(jws.Signature) | ||||||
|  | 	if nil != err { | ||||||
|  | 		err := fmt.Errorf("could not decode signature: %w", err) | ||||||
|  | 		errs = append(errs, err) | ||||||
|  | 		return errs | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	//log.Printf("\n(Verify)\nSignable: %s", signable) | ||||||
|  | 	//log.Printf("Hash: %s", hash) | ||||||
|  | 	//log.Printf("Sig: %s", jws.Signature) | ||||||
|  | 	if nil == pub { | ||||||
|  | 		err := fmt.Errorf("token signature could not be verified") | ||||||
|  | 		errs = append(errs, err) | ||||||
|  | 	} else if !Verify(pub, hash[:], sig) { | ||||||
|  | 		err := fmt.Errorf("token signature is not valid") | ||||||
|  | 		errs = append(errs, err) | ||||||
|  | 	} | ||||||
|  | 	return errs | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func selfsignCheck(jwkmap Object, errs []error) (PublicKey, []error) { | ||||||
|  | 	var pub PublicKey = nil | ||||||
|  | 	log.Println("Security TODO: did not check jws.Claims[\"sub\"] against 'jwk'") | ||||||
|  | 	log.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 = ParseJWKPublicKey(k) | ||||||
|  | 		if nil != err { | ||||||
|  | 			return nil, append(errs, 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 = ParseJWKPublicKey(k) | ||||||
|  | 		if nil != err { | ||||||
|  | 			return nil, append(errs, err) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return pub, errs | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func pubkeyCheck(pubkey PublicKey, kid string, opts *keyOptions, errs []error) (PublicKey, []error) { | ||||||
|  | 	var pub PublicKey = nil | ||||||
|  | 
 | ||||||
|  | 	if "" == kid { | ||||||
|  | 		err := errors.New("token should have 'kid' or 'jwk' in header to identify the public key") | ||||||
|  | 		errs = append(errs, err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if nil == pubkey { | ||||||
|  | 		if allowMocking { | ||||||
|  | 			if 0 == opts.mockSeed { | ||||||
|  | 				err := errors.New("the debug API requires '_seed' to accompany 'kid'") | ||||||
|  | 				errs = append(errs, err) | ||||||
|  | 			} | ||||||
|  | 			if "" == opts.KeyType { | ||||||
|  | 				err := errors.New("the debug API requires '_kty' to accompany '_seed'") | ||||||
|  | 				errs = append(errs, err) | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			if 0 == opts.mockSeed || "" == opts.KeyType { | ||||||
|  | 				return nil, errs | ||||||
|  | 			} | ||||||
|  | 			privkey := newPrivateKey(opts) | ||||||
|  | 			pub = NewPublicKey(privkey.Public()) | ||||||
|  | 			return pub, errs | ||||||
|  | 		} | ||||||
|  | 		err := errors.New("no matching public key") | ||||||
|  | 		errs = append(errs, err) | ||||||
|  | 	} else { | ||||||
|  | 		pub = pubkey | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if nil != pub && "" != kid { | ||||||
|  | 		if 1 != subtle.ConstantTimeCompare([]byte(kid), []byte(pub.Thumbprint())) { | ||||||
|  | 			err := errors.New("'kid' does not match the public key thumbprint") | ||||||
|  | 			errs = append(errs, err) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return pub, errs | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Verify will check the signature of a hash | ||||||
|  | func Verify(pubkey PublicKey, hash []byte, sig []byte) bool { | ||||||
|  | 
 | ||||||
|  | 	switch pub := pubkey.Key().(type) { | ||||||
|  | 	case *rsa.PublicKey: | ||||||
|  | 		//log.Printf("RSA VERIFY") | ||||||
|  | 		// TODO 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 { | ||||||
|  | 			return false | ||||||
|  | 		} | ||||||
|  | 		return true | ||||||
|  | 	case *ecdsa.PublicKey: | ||||||
|  | 		r := &big.Int{} | ||||||
|  | 		r.SetBytes(sig[0:32]) | ||||||
|  | 		s := &big.Int{} | ||||||
|  | 		s.SetBytes(sig[32:]) | ||||||
|  | 		return ecdsa.Verify(pub, hash, r, s) | ||||||
|  | 	default: | ||||||
|  | 		panic("impossible condition: non-rsa/non-ecdsa key") | ||||||
|  | 		//return false | ||||||
|  | 	} | ||||||
|  | } | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user