2
0
mirror of https://github.com/therootcompany/keypairs synced 2025-04-21 15:00:44 +00:00

Compare commits

...

43 Commits

Author SHA1 Message Date
88a0b05c2f update usage example 2020-11-25 03:15:34 -07:00
a62ae2ea87 add inspect with OIDC key fetch 2020-11-25 03:10:33 -07:00
be38720eba doc: update README and help 2020-10-21 04:26:53 -06:00
286cf11070 transitioning to go1.15 PublicKey interface 2020-10-21 03:38:05 -06:00
743731c804 move away from PublicKeyDeprecated 2020-10-21 03:16:34 -06:00
098f92178a move to PublicKeyTransitional, toward go1.15 keypairs 2020-10-21 01:53:40 -06:00
2e3ead4102 make -o alias of --key 2020-10-21 00:41:41 -06:00
7e6fd174ef bugfix no subcommand 2020-10-21 00:26:07 -06:00
d4f18f8c78 add goreleaser 2020-10-21 00:22:40 -06:00
f275cd82d3 update version output 2020-10-21 00:22:33 -06:00
d42763516f add verify subcommand 2020-10-02 02:44:26 -06:00
a560441c61 whitespace 2020-10-01 23:59:41 -06:00
db958734f5 update doc links and author info 2020-10-01 23:58:33 -06:00
22ba73fa12 add sign subcommand 2020-10-01 23:53:17 -06:00
a2aa6b5411 add trailing newline to output 2020-10-01 21:52:59 -06:00
94f22e1948 fix flag parsing 2020-10-01 21:45:43 -06:00
f46e11257b add CLI to generate and marshal keypair 2020-10-01 21:30:26 -06:00
db6a30b358 update documented import path 2020-05-10 13:11:26 -06:00
e4ff8ac55b update canonical import path 2020-05-10 12:34:01 -06:00
8f66f1d235 go lint and update docs 2020-04-10 13:59:44 -04:00
ignore
78847a9cfd add MIT license 2019-08-19 14:48:28 -06:00
AJ ONeal
5701d77d7a friendlier error reporting 2019-07-11 10:59:19 -06:00
AJ ONeal
fe5cf33bef update error message 2019-04-22 10:10:36 -06:00
AJ ONeal
6fc75632c8 Warn on empty string in whitelist 2019-04-17 17:08:53 -06:00
671ea1250d v0.4.0 require separate string for private issuers 2019-04-15 11:09:34 -06:00
1205ea05e1 add specific key test 2019-03-25 17:48:39 -06:00
c4fc285259 add String method to Whitelist 2019-03-22 16:17:13 -06:00
3ab2594234 make IsTrustedIssuer a method of Whitelist 2019-03-22 15:28:11 -06:00
d981fb3a7c Merge branch 'master' of github.com:big-squid/go-keypairs 2019-03-22 14:01:30 -06:00
ada07e4446 better error for private networking 2019-03-22 14:01:19 -06:00
AJ ONeal
f2d341a3c0 v0.2.0: add support for fetching /pem 2019-03-15 17:52:53 -06:00
90b05bac5f add user agent 2019-03-08 14:28:23 -07:00
ce652e0590 doc updates 2019-03-06 20:18:21 -07:00
a0d19dd83c update docs 2019-03-06 20:13:35 -07:00
109f77841b parse certificate files, and golint 2019-03-06 18:00:17 -07:00
1b938c56da add /pem 2019-03-06 17:59:42 -07:00
074b91bc2f add tests for implicit issuer 2019-03-06 14:59:25 -07:00
e753b5a1a6 added IsTrustedIssuer + tests 2019-03-06 11:08:40 -07:00
ed2297e2ad add more docs 2019-02-28 20:47:08 -07:00
6769ef90c3 add test 2019-02-28 19:40:13 -07:00
c74a56b907 minor bugfix 2019-02-28 19:16:58 -07:00
AJ ONeal
a3d33da5d0 keydist -> keyserve (better pkg name) 2019-02-28 15:32:16 -07:00
AJ ONeal
f542314cea add generic http handler for JWKs URL 2019-02-28 15:31:10 -07:00
28 changed files with 2709 additions and 173 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
/keypairs
/dist/
.DS_Store
.*.sw*

41
.goreleaser.yml Normal file
View File

@ -0,0 +1,41 @@
# This is an example goreleaser.yaml file with some sane defaults.
# Make sure to check the documentation at http://goreleaser.com
before:
hooks:
- go generate ./...
builds:
- id: keypairs
main: ./cmd/keypairs/keypairs.go
env:
- CGO_ENABLED=0
flags:
- -mod=vendor
goos:
- linux
- windows
- darwin
- freebsd
goarch:
- amd64
- arm
- arm64
archives:
- replacements:
386: i386
amd64: x86-64
arm64: aarch64
format_overrides:
- goos: windows
format: zip
env_files:
github_token: ~/.config/goreleaser/github_token.txt
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ .Tag }}-next"
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'

1
AUTHORS Normal file
View File

@ -0,0 +1 @@
AJ ONeal <aj@therootcompany.com> (https://therootcompany.com)

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
The MIT License
Copyright (c) 2018-2019 Big Squid, Inc
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

106
README.md
View File

@ -1,56 +1,102 @@
# go-keypairs # [keypairs](https://git.rootprojects.org/root/keypairs)
The lightest touch over top of Go's `crypto/ecdsa` and `crypto/rsa` to make them A cross-platform Command Line Tool and Golang Library that works
*typesafe* and to provide JSON Web Key (JWK) support. with RSA, ECDSA, PEM, DER, JWK, and the JOSE suite.
# Documentation # Keypairs CLI
Use the source, Luke! Generates, signs, and verifies with NIST-strength asymmetric keys.
<https://godoc.org/github.com/big-squid/go-keypairs> ```bash
# Generate JSON Web Keys (JWKs)
keypairs gen > key.jwk.json 2> pub.jwk.json
# Generate PEM (or DER) Keys, by extension
keypairs gen --key key.pem --pub pub.pem
# Sign a payload
keypairs sign key.jwk.json --exp 1h '{ "sub": "me@example.com" }' > token.jwt 2> sig.jws
# Verify a signature
keypairs verify pub.jwk.json token.jwt
```
Cheat Sheet at <https://webinstall.dev/keypairs>.
### Install
**Mac**, **Linux**:
```bash
curl -sS https://webinstall.dev/keypairs | bash
```
**Windows 10**:
```bash
curl.exe -A MS https://webinstall.dev/keypairs | powershell
```
# Keypairs Go Library
JSON Web Key (JWK) support and type safety lightly placed over top of Go's `crypto/ecdsa` and `crypto/rsa`
Useful for JWT, JOSE, etc.
```go
key, err := keypairs.ParsePrivateKey(bytesForJWKOrPEMOrDER)
pub, err := keypairs.ParsePublicKey(bytesForJWKOrPEMOrDER)
jwk, err := keypairs.MarshalJWKPublicKey(pub, time.Now().Add(2 * time.Day))
kid, err := keypairs.ThumbprintPublicKey(pub)
```
# GoDoc API Documentation
See <https://pkg.go.dev/git.rootprojects.org/root/keypairs>
# Philosophy # Philosophy
Always remember: Go's standard library is great.
> Don't roll your own crypto. Go has _excellent_ crytography support and provides wonderful
primitives for dealing with them.
But also remember: I prefer to stay as close to Go's `crypto` package as possible,
just adding a light touch for JWT support and type safety.
> Just because you _don't_ know someone doesn't make them smart.
Don't get the two mixed up!
(furthermore, [just because you _do_ know someone doesn't make them _not_ smart](https://www.humancondition.com/asid-prophets-without-honour-in-their-own-home/))
Although I would not want to invent my own cryptographic algorithm,
I've read enough source code to know that, for standards I know well,
I feel much more confident in the security, extensibility, and documentation
of tooling that I've write myself.
# Type Safety # Type Safety
Go has _excellent_ crytography support and provides wonderful `crypto.PublicKey` is a "marker interface", meaning that it is **not typesafe**!
primitives for dealing with them. Its Achilles' heel is they're **not typesafe**!
As of Go 1.11.5 `crypto.PublicKey` and `crypto.PrivateKey` are "marker interfaces" `go-keypairs` defines `type keypairs.PrivateKey interface { Public() crypto.PublicKey }`,
or, in other words, empty interfaces that only serve to document intent without
actually providing a constraint to the type system.
go-keypairs defines `type keypairs.PrivateKey interface { Public() crypto.PublicKey }`,
which is implemented by `crypto/rsa` and `crypto/ecdsa` which is implemented by `crypto/rsa` and `crypto/ecdsa`
(but not `crypto/dsa`, which we really don't care that much about). (but not `crypto/dsa`, which we really don't care that much about).
Go1.15 will add `[PublicKey.Equal(crypto.PublicKey)](https://github.com/golang/go/issues/21704)`,
which will make it possible to remove the additional wrapper over `PublicKey`
and use an interface instead.
Since there are no common methods between `rsa.PublicKey` and `ecdsa.PublicKey`, Since there are no common methods between `rsa.PublicKey` and `ecdsa.PublicKey`,
go-keypairs lightly wraps each to implement `Thumbprint() string` (part of the JOSE/JWK spec). go-keypairs lightly wraps each to implement `Thumbprint() string` (part of the JOSE/JWK spec).
# JSON Web Key "codec" ## JSON Web Key (JWK) as a "codec"
Although there are many, many ways that JWKs could be interpreted Although there are many, many ways that JWKs could be interpreted
(possibly why they haven't made it into the standard library), go-keypairs (possibly why they haven't made it into the standard library), `go-keypairs`
follows the basic pattern of `encoding/x509` to Parse and Marshal follows the basic pattern of `encoding/x509` to `Parse` and `Marshal`
only the most basic and most meaningful parts of a key. only the most basic and most meaningful parts of a key.
I highly recommend that you use `Thumbprint()` for `KeyID` you also I highly recommend that you use `Thumbprint()` for `KeyID` you also
get the benefit of not losing information when encoding and decoding get the benefit of not losing information when encoding and decoding
between the ASN.1, x509, PEM, and JWK formats. between the ASN.1, x509, PEM, and JWK formats.
# LICENSE
Copyright (c) 2020-present AJ ONeal \
Copyright (c) 2018-2019 Big Squid, Inc.
This work is licensed under the terms of the MIT license. \
For a copy, see <https://opensource.org/licenses/MIT>.

19
cli_test.sh Normal file
View 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

477
cmd/keypairs/keypairs.go Normal file
View File

@ -0,0 +1,477 @@
package main
import (
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"os"
"strings"
"time"
"git.rootprojects.org/root/keypairs"
"git.rootprojects.org/root/keypairs/keyfetch"
)
var (
name = "keypairs"
version = "0.0.0"
date = "0001-01-01T00:00:00Z"
commit = "0000000"
)
func usage() {
fmt.Println(ver())
fmt.Println()
fmt.Println("Usage")
fmt.Printf(" %s <command> [flags] args...\n", name)
fmt.Println("")
fmt.Printf("See usage: %s help <command>\n", name)
fmt.Println("")
fmt.Println("Commands:")
fmt.Println(" version")
fmt.Println(" gen")
fmt.Println(" sign")
fmt.Println(" inspect (decode)")
fmt.Println(" verify")
fmt.Println("")
fmt.Println("Examples:")
fmt.Println(" keypairs gen --key 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 '{ \"sub\": \"xxxx\" }'")
fmt.Println("")
fmt.Println(" keypairs inspect --verbose 'xxxx.yyyy.zzzz'")
fmt.Println("")
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() string {
return fmt.Sprintf("%s v%s (%s) %s", name, version, commit[:7], date)
}
func main() {
args := os.Args[:]
if len(args) < 2 || "help" == args[1] {
// top-level help
if len(args) <= 2 {
usage()
os.Exit(0)
return
}
// move help to subcommand argument
self := args[0]
args = append([]string{self}, args[2:]...)
args = append(args, "--help")
}
switch args[1] {
case "version":
fmt.Println(ver())
os.Exit(0)
return
case "gen":
gen(args[2:])
case "sign":
sign(args[2:])
case "decode":
fallthrough
case "inspect":
inspect(args[2:])
case "verify":
verify(args[2:])
default:
usage()
os.Exit(1)
return
}
}
func gen(args []string) {
var keyname string
var keynameAlt string
//var keynameAlt2 string
var pubname string
flags := flag.NewFlagSet("gen", flag.ExitOnError)
flags.StringVar(&keynameAlt, "o", "", "output file (alias of --key)")
//flags.StringVar(&keynameAlt2, "priv", "", "private key file (alias of --key)")
flags.StringVar(&keyname, "key", "", "private key file (ex: key.jwk.json or key.pem)")
flags.StringVar(&pubname, "pub", "", "public key file (ex: pub.jwk.json or pub.pem)")
flags.Parse(args)
if 0 == len(keyname) {
keyname = keynameAlt
}
/*
if 0 == len(keyname) {
keyname = keynameAlt2
}
*/
key := keypairs.NewDefaultPrivateKey()
marshalPriv(key, keyname)
pub := key.Public().(keypairs.PublicKey)
marshalPub(pub, pubname)
}
func sign(args []string) {
var exp time.Duration
flags := flag.NewFlagSet("sign", flag.ExitOnError)
flags.DurationVar(&exp, "exp", 0, "duration until token expires (Default 15m)")
flags.Parse(args)
if len(flags.Args()) <= 1 {
fmt.Fprintf(os.Stderr, "Usage: keypairs sign --exp 1h <private PEM or JWK> ./payload.json\n")
os.Exit(1)
}
keyname := flags.Args()[0]
payload := flags.Args()[1]
key, err := readKey(keyname)
if nil != err {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
return
}
if "" == payload {
// TODO should this be null? I forget
payload = "{}"
}
b, err := ioutil.ReadFile(payload)
claims := map[string]interface{}{}
if nil != err {
var err2 error
err2 = json.Unmarshal([]byte(payload), &claims)
if nil != err2 {
fmt.Fprintf(os.Stderr,
"could not read payload as file (or parse as string) %q: %s\n", payload, err)
os.Exit(1)
return
}
}
if 0 == len(claims) {
var err3 error
err3 = json.Unmarshal(b, &claims)
if nil != err3 {
fmt.Fprintf(os.Stderr,
"could not parse palyoad from file %q: %s\n", payload, err3)
os.Exit(1)
return
}
}
if 0 != exp {
claims["exp"] = exp.Seconds()
}
if _, ok := claims["exp"]; !ok {
claims["exp"] = (15 * time.Minute).Seconds()
}
jws, err := keypairs.SignClaims(key, nil, claims)
if nil != err {
fmt.Fprintf(os.Stderr, "could not sign claims: %v\n%#v\n", err, claims)
os.Exit(1)
return
}
b, _ = json.Marshal(&jws)
fmt.Fprintf(os.Stderr, "%s\n", indentJSON(b))
fmt.Fprintf(os.Stdout, "%s\n", keypairs.JWSToJWT(jws))
}
func inspect(args []string) {
var verbose bool
flags := flag.NewFlagSet("inspect", flag.ExitOnError)
flags.BoolVar(&verbose, "verbose", true, "print extra info")
flags.Usage = func() {
fmt.Println("Usage: keypairs inspect --verbose <jwt-or-jwt>")
fmt.Println("")
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)
return
}
payload := flags.Args()[0]
jws, err := readJWS(payload)
if nil != err {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
return
}
var pub keypairs.PublicKey = nil
// because interfaces are never truly nil
hasPub := false
jwk, _ := jws.Header["jwk"].(map[string]interface{})
jwkE, _ := jwk["e"].(string)
jwkX, _ := jwk["x"].(string)
kid, _ := jws.Header["kid"].(string)
if len(jwkE) > 0 || len(jwkX) > 0 {
// TODO verify self-signed certificate
//b, _ := json.MarshalIndent(&jwk, "", " ")
if len(kid) > 0 {
fmt.Fprintf(os.Stderr, "[warn] jws header has both 'kid' (Key ID) and 'jwk' (for self-signed only)\n")
} else {
fmt.Fprintf(os.Stderr, "[debug] token is self-signed (jwk)\n")
//pub = pubx
//hasPub = true
}
} else if len(kid) > 0 {
iss, _ := jws.Claims["iss"].(string)
if strings.HasPrefix(iss, "http:") || strings.HasPrefix(iss, "https:") {
//fmt.Printf("iss: %s\n", iss)
//fmt.Printf("kid: %s\n", kid)
fmt.Fprintf(os.Stderr, "Checking for OIDC key... ")
pubx, err := keyfetch.OIDCJWK(kid, iss)
if nil != err {
fmt.Fprintf(os.Stderr, "not found.\n")
// ignore
} else {
fmt.Fprintf(os.Stderr, "found:\n")
if verbose {
b := keypairs.MarshalJWKPublicKey(pubx)
fmt.Fprintf(os.Stderr, "%s\n", indentJSON(b))
}
pub = pubx
hasPub = true
}
}
}
validSig := false
if hasPub {
errs := keypairs.VerifyClaims(pub, jws)
if len(errs) > 0 {
fmt.Fprintf(os.Stderr, "error:\n")
for _, err := range errs {
fmt.Fprintf(os.Stderr, "\t%v\n", err)
}
} else {
validSig = true
}
}
b, _ := json.MarshalIndent(&jws, "", " ")
fmt.Fprintf(os.Stdout, "%s\n", b)
if validSig {
fmt.Fprintf(os.Stderr, "Signature is Valid\n")
}
}
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)
return
}
if 1 == len(flags.Args()) {
inspect(args)
return
}
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!
pub2, 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,
)
}
pub = pub2.Key()
}
// Oh, it was a file.
if nil == pub {
pub3, err3 := keypairs.ParsePublicKey(b)
if nil != err3 {
return nil, fmt.Errorf(
"could not parse public key from file %q:\n%w",
pubname, err3,
)
}
pub = pub3.Key()
}
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) {
if "" == keyname {
b := indentJSON(keypairs.MarshalJWKPrivateKey(key))
fmt.Fprintf(os.Stdout, string(b)+"\n")
return
}
var b []byte
if strings.HasSuffix(keyname, ".json") {
b = indentJSON(keypairs.MarshalJWKPrivateKey(key))
} else if strings.HasSuffix(keyname, ".pem") {
b, _ = keypairs.MarshalPEMPrivateKey(key)
} else if strings.HasSuffix(keyname, ".der") {
b, _ = keypairs.MarshalDERPrivateKey(key)
} else {
fmt.Fprintf(os.Stderr, "private key extension should be .jwk.json, .pem, or .der")
os.Exit(1)
return
}
ioutil.WriteFile(keyname, b, 0600)
}
func marshalPub(pub keypairs.PublicKey, pubname string) {
var b []byte
if "" == pubname {
b = indentJSON(keypairs.MarshalJWKPublicKey(pub))
fmt.Fprintf(os.Stderr, string(b)+"\n")
return
}
if strings.HasSuffix(pubname, ".json") {
b = indentJSON(keypairs.MarshalJWKPublicKey(pub))
} else if strings.HasSuffix(pubname, ".pem") {
b, _ = keypairs.MarshalPEMPublicKey(pub)
} else if strings.HasSuffix(pubname, ".der") {
b, _ = keypairs.MarshalDERPublicKey(pub)
}
ioutil.WriteFile(pubname, b, 0644)
}
func indentJSON(b []byte) []byte {
m := map[string]interface{}{}
_ = json.Unmarshal(b, &m)
b, _ = json.MarshalIndent(&m, "", " ")
return append(b, '\n')
}

2
doc.go
View File

@ -17,7 +17,7 @@ Convenience functions are available which will fetch keys
(or retrieve them from cache) via OIDC, .well-known/jwks.json, and direct urls. (or retrieve them from cache) via OIDC, .well-known/jwks.json, and direct urls.
All keys are cached by Thumbprint, as well as kid(@issuer), if available. All keys are cached by Thumbprint, as well as kid(@issuer), if available.
import "github.com/big-squid/go-keypairs/keyfetch" import "git.rootprojects.org/root/keypairs/keyfetch"
pubs, err := keyfetch.OIDCJWKs("https://example.com/") pubs, err := keyfetch.OIDCJWKs("https://example.com/")
pubs, err := keyfetch.OIDCJWK(ThumbOrKeyID, "https://example.com/") pubs, err := keyfetch.OIDCJWK(ThumbOrKeyID, "https://example.com/")

5
examples/go.mod Normal file
View File

@ -0,0 +1,5 @@
module github.com/example/foobar
go 1.12
replace github.com/big-squid/go-keypairs => git.rootprojects.org/root/keypairs

View File

@ -0,0 +1,6 @@
{
"kty": "EC",
"crv": "P-256",
"x": "IT1SWLxsacPiE5Z16jkopAn8_-85rMjgyCokrnjDft4",
"y": "mP2JwOAOdMmXuwpxbKng3KZz27mz-nKWIlXJ3rzSGMo"
}

69
generate.go Normal file
View File

@ -0,0 +1,69 @@
package keypairs
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"io"
mathrand "math/rand"
"time"
)
var randReader io.Reader = rand.Reader
var allowMocking = false
// KeyOptions are the things that we may need to know about a request to fulfill it properly
type keyOptions struct {
//Key string `json:"key"`
KeyType string `json:"kty"`
mockSeed int64 //`json:"-"`
//SeedStr string `json:"seed"`
//Claims Object `json:"claims"`
//Header Object `json:"header"`
}
func (o *keyOptions) nextReader() io.Reader {
if allowMocking {
return o.maybeMockReader()
}
return randReader
}
// NewDefaultPrivateKey generates a key with reasonable strength.
// Today that means a 256-bit equivalent - either RSA 2048 or EC P-256.
func NewDefaultPrivateKey() PrivateKey {
// insecure random is okay here,
// it's just used for a coin toss
mathrand.Seed(time.Now().UnixNano())
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 {
return newPrivateKey(&keyOptions{
KeyType: "RSA",
})
}
return newPrivateKey(&keyOptions{
KeyType: "EC",
})
}
// newPrivateKey generates a 256-bit entropy RSA or ECDSA private key
func newPrivateKey(opts *keyOptions) PrivateKey {
var privkey PrivateKey
if "RSA" == opts.KeyType {
keylen := 2048
privkey, _ = rsa.GenerateKey(opts.nextReader(), keylen)
if allowMocking {
privkey = maybeDerandomizeMockKey(privkey, keylen, opts)
}
} else {
// TODO: EC keys may also suffer the same random problems in the future
privkey, _ = ecdsa.GenerateKey(elliptic.P256(), opts.nextReader())
}
return privkey
}

4
go.mod
View File

@ -1 +1,3 @@
module github.com/big-squid/go-keypairs module git.rootprojects.org/root/keypairs
go 1.12

69
jwk.go Normal file
View 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
View 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
}

View File

@ -1,23 +1,47 @@
// Package keyfetch retrieve and cache PublicKeys
// from OIDC (https://example.com/.well-known/openid-configuration)
// and Auth0 (https://example.com/.well-known/jwks.json)
// JWKs URLs and expires them when `exp` is reached
// (or a default expiry if the key does not provide one).
// It uses the keypairs package to Unmarshal the JWKs into their
// native types (with a very thin shim to provide the type safety
// that Go's crypto.PublicKey and crypto.PrivateKey interfaces lack).
package keyfetch package keyfetch
import ( import (
"errors" "errors"
"fmt" "fmt"
"log"
"net/http"
"net/url"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
keypairs "github.com/big-squid/go-keypairs" "git.rootprojects.org/root/keypairs"
"github.com/big-squid/go-keypairs/keyfetch/uncached" "git.rootprojects.org/root/keypairs/keyfetch/uncached"
) )
var EInvalidJWKURL = errors.New("url does not lead to valid JWKs") // TODO should be ErrInvalidJWKURL
// ErrInvalidJWKURL means that the url did not provide JWKs
var ErrInvalidJWKURL = errors.New("url does not lead to valid JWKs")
// KeyCache is an in-memory key cache
var KeyCache = map[string]CachableKey{} var KeyCache = map[string]CachableKey{}
// KeyCacheMux is used to guard the in-memory cache
var KeyCacheMux = sync.Mutex{} var KeyCacheMux = sync.Mutex{}
// ErrInsecureDomain means that plain http was used where https was expected
var ErrInsecureDomain = errors.New("Whitelists should only allow secure URLs (i.e. https://). To allow unsecured private networking (i.e. Docker) pass PrivateWhitelist as a list of private URLs")
// TODO Cacheable key (shouldn't this be private)?
// CachableKey represents
type CachableKey struct { type CachableKey struct {
Key keypairs.PublicKey Key keypairs.PublicKeyDeprecated
Expiry time.Time Expiry time.Time
} }
@ -43,50 +67,64 @@ type ID interface {
} }
*/ */
// StaleTime defines when public keys should be renewed (15 minutes by default)
var StaleTime = 15 * time.Minute var StaleTime = 15 * time.Minute
// DefaultKeyDuration defines how long a key should be considered fresh (48 hours by default)
var DefaultKeyDuration = 48 * time.Hour var DefaultKeyDuration = 48 * time.Hour
// MinimumKeyDuration defines the minimum time that a key will be cached (1 hour by default)
var MinimumKeyDuration = time.Hour var MinimumKeyDuration = time.Hour
// MaximumKeyDuration defines the maximum time that a key will be cached (72 hours by default)
var MaximumKeyDuration = 72 * time.Hour var MaximumKeyDuration = 72 * time.Hour
type publicKeysMap map[string]keypairs.PublicKey // PublicKeysMap is a newtype for a map of keypairs.PublicKey
type PublicKeysMap = map[string]keypairs.PublicKeyDeprecated
// FetchOIDCPublicKeys fetches baseURL + ".well-known/openid-configuration" and then returns FetchPublicKeys(jwks_uri). // OIDCJWKs fetches baseURL + ".well-known/openid-configuration" and then fetches and returns the Public Keys.
func OIDCJWKs(baseURL string) (publicKeysMap, error) { func OIDCJWKs(baseURL string) (PublicKeysMap, error) {
if maps, keys, err := uncached.OIDCJWKs(baseURL); nil != err { maps, keys, err := uncached.OIDCJWKs(baseURL)
if nil != err {
return nil, err return nil, err
} else { }
cacheKeys(maps, keys, baseURL) cacheKeys(maps, keys, baseURL)
return keys, err return keys, err
}
} }
// OIDCJWK fetches baseURL + ".well-known/openid-configuration" and then returns the key matching kid (or thumbprint)
func OIDCJWK(kidOrThumb, iss string) (keypairs.PublicKey, error) { func OIDCJWK(kidOrThumb, iss string) (keypairs.PublicKey, error) {
return immediateOneOrFetch(kidOrThumb, iss, uncached.OIDCJWKs) return immediateOneOrFetch(kidOrThumb, iss, uncached.OIDCJWKs)
} }
func WellKnownJWKs(kidOrThumb, iss string) (publicKeysMap, error) { // WellKnownJWKs fetches baseURL + ".well-known/jwks.json" and caches and returns the keys
if maps, keys, err := uncached.WellKnownJWKs(iss); nil != err { func WellKnownJWKs(kidOrThumb, iss string) (PublicKeysMap, error) {
maps, keys, err := uncached.WellKnownJWKs(iss)
if nil != err {
return nil, err return nil, err
} else { }
cacheKeys(maps, keys, iss) cacheKeys(maps, keys, iss)
return keys, err return keys, err
}
} }
// WellKnownJWK fetches baseURL + ".well-known/jwks.json" and returns the key matching kid (or thumbprint)
func WellKnownJWK(kidOrThumb, iss string) (keypairs.PublicKey, error) { func WellKnownJWK(kidOrThumb, iss string) (keypairs.PublicKey, error) {
return immediateOneOrFetch(kidOrThumb, iss, uncached.WellKnownJWKs) return immediateOneOrFetch(kidOrThumb, iss, uncached.WellKnownJWKs)
} }
// JWKs returns a map of keys identified by their thumbprint // JWKs returns a map of keys identified by their thumbprint
// (since kid may or may not be present) // (since kid may or may not be present)
func JWKs(jwksurl string) (publicKeysMap, error) { func JWKs(jwksurl string) (PublicKeysMap, error) {
if maps, keys, err := uncached.JWKs(jwksurl); nil != err { maps, keys, err := uncached.JWKs(jwksurl)
if nil != err {
return nil, err return nil, err
} else { }
iss := strings.Replace(jwksurl, ".well-known/jwks.json", "", 1) iss := strings.Replace(jwksurl, ".well-known/jwks.json", "", 1)
cacheKeys(maps, keys, iss) cacheKeys(maps, keys, iss)
return keys, err return keys, err
}
} }
// JWK tries to return a key from cache, falling back to the /.well-known/jwks.json of the issuer // JWK tries to return a key from cache, falling back to the /.well-known/jwks.json of the issuer
@ -94,10 +132,44 @@ func JWK(kidOrThumb, iss string) (keypairs.PublicKey, error) {
return immediateOneOrFetch(kidOrThumb, iss, uncached.JWKs) return immediateOneOrFetch(kidOrThumb, iss, uncached.JWKs)
} }
// PEM tries to return a key from cache, falling back to the specified PEM url
func PEM(url string) (keypairs.PublicKey, error) {
// url is kid in this case
return immediateOneOrFetch(url, url, func(string) (map[string]map[string]string, map[string]keypairs.PublicKeyDeprecated, error) {
m, key, err := uncached.PEM(url)
if nil != err {
return nil, nil, err
}
pubd := keypairs.NewPublicKey(key)
// TODO bring this back
switch p := pubd.(type) {
case *keypairs.ECPublicKey:
p.KID = url
case *keypairs.RSAPublicKey:
p.KID = url
default:
return nil, nil, errors.New("impossible key type")
}
// put in a map, just for caching
maps := map[string]map[string]string{}
maps[keypairs.Thumbprint(key)] = m
maps[url] = m
keys := uncached.PublicKeysMap{} // map[string]keypairs.PublicKeyDeprecated{}
keys[keypairs.Thumbprint(key)] = pubd
keys[url] = pubd
return maps, keys, nil
})
}
// Fetch returns a key from cache, falling back to an exact url as the "issuer" // Fetch returns a key from cache, falling back to an exact url as the "issuer"
func Fetch(url string) (keypairs.PublicKey, error) { func Fetch(url string) (keypairs.PublicKey, error) {
// url is kid in this case // url is kid in this case
return immediateOneOrFetch(url, url, func(string) (map[string]map[string]string, map[string]keypairs.PublicKey, error) { return immediateOneOrFetch(url, url,
func(string) (map[string]map[string]string, map[string]keypairs.PublicKeyDeprecated, error) {
m, key, err := uncached.Fetch(url) m, key, err := uncached.Fetch(url)
if nil != err { if nil != err {
return nil, nil, err return nil, nil, err
@ -105,10 +177,10 @@ func Fetch(url string) (keypairs.PublicKey, error) {
// put in a map, just for caching // put in a map, just for caching
maps := map[string]map[string]string{} maps := map[string]map[string]string{}
maps[key.Thumbprint()] = m maps[keypairs.Thumbprint(key.Key())] = m
keys := map[string]keypairs.PublicKey{} keys := map[string]keypairs.PublicKeyDeprecated{}
keys[key.Thumbprint()] = key keys[keypairs.Thumbprint(key.Key())] = key
return maps, keys, nil return maps, keys, nil
}) })
@ -118,7 +190,7 @@ func Fetch(url string) (keypairs.PublicKey, error) {
// The issuer string may be empty if using a thumbprint rather than a kid. // The issuer string may be empty if using a thumbprint rather than a kid.
func Get(kidOrThumb, iss string) keypairs.PublicKey { func Get(kidOrThumb, iss string) keypairs.PublicKey {
if pub := get(kidOrThumb, iss); nil != pub { if pub := get(kidOrThumb, iss); nil != pub {
return pub.Key return pub.Key.Key()
} }
return nil return nil
} }
@ -152,21 +224,21 @@ func get(kidOrThumb, iss string) *CachableKey {
func immediateOneOrFetch(kidOrThumb, iss string, fetcher myfetcher) (keypairs.PublicKey, error) { func immediateOneOrFetch(kidOrThumb, iss string, fetcher myfetcher) (keypairs.PublicKey, error) {
now := time.Now() now := time.Now()
key := get(kidOrThumb, iss) hit := get(kidOrThumb, iss)
if nil == key { if nil == hit {
return fetchAndSelect(kidOrThumb, iss, fetcher) return fetchAndSelect(kidOrThumb, iss, fetcher)
} }
// Fetch just a little before the key actually expires // Fetch just a little before the key actually expires
if key.Expiry.Sub(now) <= StaleTime { if hit.Expiry.Sub(now) <= StaleTime {
go fetchAndSelect(kidOrThumb, iss, fetcher) go fetchAndSelect(kidOrThumb, iss, fetcher)
} }
return key.Key, nil return hit.Key.Key(), nil
} }
type myfetcher func(string) (map[string]map[string]string, map[string]keypairs.PublicKey, error) type myfetcher func(string) (map[string]map[string]string, map[string]keypairs.PublicKeyDeprecated, error)
func fetchAndSelect(id, baseURL string, fetcher myfetcher) (keypairs.PublicKey, error) { func fetchAndSelect(id, baseURL string, fetcher myfetcher) (keypairs.PublicKey, error) {
maps, keys, err := fetcher(baseURL) maps, keys, err := fetcher(baseURL)
@ -177,20 +249,21 @@ func fetchAndSelect(id, baseURL string, fetcher myfetcher) (keypairs.PublicKey,
for i := range keys { for i := range keys {
key := keys[i] key := keys[i]
pub := key.Key()
if id == key.Thumbprint() { if id == keypairs.Thumbprint(pub) {
return key, nil return pub, nil
} }
if id == key.KeyID() { if id == key.KeyID() {
return key, nil return pub, nil
} }
} }
return nil, fmt.Errorf("Key identified by '%s' was not found at %s", id, baseURL) return nil, fmt.Errorf("Key identified by '%s' was not found at %s", id, baseURL)
} }
func cacheKeys(maps map[string]map[string]string, keys map[string]keypairs.PublicKey, issuer string) { func cacheKeys(maps map[string]map[string]string, keys PublicKeysMap, issuer string) {
for i := range keys { for i := range keys {
key := keys[i] key := keys[i]
m := maps[i] m := maps[i]
@ -200,10 +273,13 @@ func cacheKeys(maps map[string]map[string]string, keys map[string]keypairs.Publi
} }
iss = normalizeIssuer(iss) iss = normalizeIssuer(iss)
cacheKey(m["kid"], iss, m["exp"], key) cacheKey(m["kid"], iss, m["exp"], key)
if 0 == len(m[uncached.URLishKey]) {
cacheKey(m[uncached.URLishKey], iss, m["exp"], key)
}
} }
} }
func cacheKey(kid, iss, expstr string, pub keypairs.PublicKey) error { func cacheKey(kid, iss, expstr string, pub keypairs.PublicKeyDeprecated) error {
var expiry time.Time var expiry time.Time
iss = normalizeIssuer(iss) iss = normalizeIssuer(iss)
@ -227,7 +303,7 @@ func cacheKey(kid, iss, expstr string, pub keypairs.PublicKey) error {
Expiry: expiry, Expiry: expiry,
} }
// Since thumbprints are crypto secure, iss isn't needed // Since thumbprints are crypto secure, iss isn't needed
thumb := pub.Thumbprint() thumb := keypairs.Thumbprint(pub.Key())
KeyCache[thumb] = CachableKey{ KeyCache[thumb] = CachableKey{
Key: pub, Key: pub,
Expiry: expiry, Expiry: expiry,
@ -245,3 +321,212 @@ func clear() {
func normalizeIssuer(iss string) string { func normalizeIssuer(iss string) string {
return strings.TrimRight(iss, "/") return strings.TrimRight(iss, "/")
} }
func isTrustedIssuer(iss string, whitelist Whitelist, rs ...*http.Request) bool {
if "" == iss {
return false
}
// Normalize the http:// and https:// and parse
iss = strings.TrimRight(iss, "/") + "/"
if strings.HasPrefix(iss, "http://") {
// ignore
} else if strings.HasPrefix(iss, "//") {
return false // TODO
} else if !strings.HasPrefix(iss, "https://") {
iss = "https://" + iss
}
issURL, err := url.Parse(iss)
if nil != err {
return false
}
// Check that
// * schemes match (https: == https:)
// * paths match (/foo/ == /foo/, always with trailing slash added)
// * hostnames are compatible (a == b or "sub.foo.com".HasSufix(".foo.com"))
for i := range []*url.URL(whitelist) {
u := whitelist[i]
if issURL.Scheme != u.Scheme {
continue
} else if u.Path != strings.TrimRight(issURL.Path, "/")+"/" {
continue
} else if issURL.Host != u.Host {
if '.' == u.Host[0] && strings.HasSuffix(issURL.Host, u.Host) {
return true
}
continue
}
// All failures have been handled
return true
}
// Check if implicit issuer is available
if 0 == len(rs) {
return false
}
return hasImplicitTrust(issURL, rs[0])
}
// hasImplicitTrust relies on the security of DNS and TLS to determine if the
// headers of the request can be trusted as identifying the server itself as
// a valid issuer, without additional configuration.
//
// Helpful for testing, but in the wrong hands could easily lead to a zero-day.
func hasImplicitTrust(issURL *url.URL, r *http.Request) bool {
if nil == r {
return false
}
// Sanity check that, if a load balancer exists, it isn't misconfigured
proto := r.Header.Get("X-Forwarded-Proto")
if "" != proto && proto != "https" {
return false
}
// Get the host
// * If TLS, block Domain Fronting
// * Otherwise assume trusted proxy
// * Otherwise assume test environment
var host string
if nil != r.TLS {
// Note that if this were to be implemented for HTTP/2 it would need to
// check all names on the certificate, not just the one with which the
// original connection was established. However, not our problem here.
// See https://serverfault.com/a/908087/93930
if r.TLS.ServerName != r.Host {
return false
}
host = r.Host
} else {
host = r.Header.Get("X-Forwarded-Host")
if "" == host {
host = r.Host
}
}
// Same tests as above, adjusted since it can't handle wildcards and, since
// the path is variable, we make the assumption that a child can trust a
// parent, but that a parent cannot trust a child.
if r.Host != issURL.Host {
return false
}
if !strings.HasPrefix(strings.TrimRight(r.URL.Path, "/")+"/", issURL.Path) {
// Ex: Request URL Token Issuer
// !"https:example.com/johndoe/api/dothing".HasPrefix("https:example.com/")
return false
}
return true
}
// Whitelist is a newtype for an array of URLs
type Whitelist []*url.URL
// NewWhitelist turns an array of URLs (such as https://example.com/) into
// a parsed array of *url.URLs that can be used by the IsTrustedIssuer function
func NewWhitelist(issuers []string, privateList ...[]string) (Whitelist, error) {
var err error
list := []*url.URL{}
if 0 != len(issuers) {
insecure := false
list, err = newWhitelist(list, issuers, insecure)
if nil != err {
return nil, err
}
}
if 0 != len(privateList) && 0 != len(privateList[0]) {
insecure := true
list, err = newWhitelist(list, privateList[0], insecure)
if nil != err {
return nil, err
}
}
return Whitelist(list), nil
}
func newWhitelist(list []*url.URL, issuers []string, insecure bool) (Whitelist, error) {
for i := range issuers {
iss := issuers[i]
if "" == strings.TrimSpace(iss) {
fmt.Println("[Warning] You have an empty string in your keyfetch whitelist.")
continue
}
// Should have a valid http or https prefix
// TODO support custom prefixes (i.e. app://) ?
if strings.HasPrefix(iss, "http://") {
if !insecure {
log.Println("Oops! You have an insecure domain in your whitelist: ", iss)
return nil, ErrInsecureDomain
}
} else if strings.HasPrefix(iss, "//") {
// TODO
return nil, errors.New("Rather than prefixing with // to support multiple protocols, add them seperately:" + iss)
} else if !strings.HasPrefix(iss, "https://") {
iss = "https://" + iss
}
// trailing slash as a boundary character, which may or may not denote a directory
iss = strings.TrimRight(iss, "/") + "/"
u, err := url.Parse(iss)
if nil != err {
return nil, err
}
// Strip any * prefix, for easier comparison later
// *.example.com => .example.com
if strings.HasPrefix(u.Host, "*.") {
u.Host = u.Host[1:]
}
list = append(list, u)
}
return list, nil
}
/*
IsTrustedIssuer returns true when the `iss` (i.e. from a token) matches one
in the provided whitelist (also matches wildcard domains).
You may explicitly allow insecure http (i.e. for automated testing) by
including http:// Otherwise the scheme in each item of the whitelist should
include the "https://" prefix.
SECURITY CONSIDERATIONS (Please Read)
You'll notice that *http.Request is optional. It should only be used under these
three circumstances:
1) Something else guarantees http -> https redirection happens before the
connection gets here AND this server directly handles TLS/SSL.
2) If you're using a load balancer or web server, and this doesn't handle
TLS/SSL directly, that server is _explicitly_ configured to protect
against Domain Fronting attacks. As of 2019, most web servers and load
balancers do not protect against that by default.
3) If you only use it to make your automated integration testing more
and it isn't enabled in production.
Otherwise, DO NOT pass in *http.Request as you will introduce a 0-day
vulnerability allowing an attacker to spoof any token issuer of their choice.
The only reason I allowed this in a public library where non-experts would
encounter it is to make testing easier.
*/
func (w Whitelist) IsTrustedIssuer(iss string, rs ...*http.Request) bool {
return isTrustedIssuer(iss, w, rs...)
}
// String will generate a space-delimited list of whitelisted URLs
func (w Whitelist) String() string {
s := []string{}
for i := range w {
s = append(s, w[i].String())
}
return strings.Join(s, " ")
}

View File

@ -4,14 +4,38 @@ import (
"testing" "testing"
"time" "time"
keypairs "github.com/big-squid/go-keypairs" "git.rootprojects.org/root/keypairs"
"github.com/big-squid/go-keypairs/keyfetch/uncached" "git.rootprojects.org/root/keypairs/keyfetch/uncached"
) )
var pubkey keypairs.PublicKey
func TestCachesKey(t *testing.T) { func TestCachesKey(t *testing.T) {
// TODO set KeyID() in cache
testCachesKey(t, "https://bigsquid.auth0.com/") testCachesKey(t, "https://bigsquid.auth0.com/")
clear() clear()
testCachesKey(t, "https://bigsquid.auth0.com") testCachesKey(t, "https://bigsquid.auth0.com")
// Get PEM
pubk3, err := PEM("https://bigsquid.auth0.com/pem")
if nil != err {
t.Fatal("[0] Error fetching and caching key:", err)
}
thumb3 := keypairs.Thumbprint(pubk3)
thumb := keypairs.Thumbprint(pubkey)
if thumb3 != thumb {
t.Fatalf("Error got different thumbprint for different versions of the same key %q != %q: %v", thumb3, thumb, err)
}
clear()
testCachesKey(t, "https://big-squid.github.io/")
}
func TestKnownKID(t *testing.T) {
url := "https://kraken-dev.auth0.com"
kid := "RkVGNTM5NDc4NkM4NjA5OEMxMTNCMTNBQ0RGRDA0MEQ0RDNDMkM3Qw"
_, err := OIDCJWK(kid, url)
if nil != err {
t.Fatal(url, err)
}
} }
func testCachesKey(t *testing.T, url string) { func testCachesKey(t *testing.T, url string) {
@ -26,10 +50,10 @@ func testCachesKey(t *testing.T, url string) {
var key keypairs.PublicKey var key keypairs.PublicKey
for i := range keys { for i := range keys {
key = keys[i] key = keys[i].Key()
break break
} }
thumb := key.Thumbprint() thumb := keypairs.Thumbprint(key)
// Look in cache for each (and fail) // Look in cache for each (and fail)
if pub := Get(thumb, ""); nil != pub { if pub := Get(thumb, ""); nil != pub {
@ -37,22 +61,27 @@ func testCachesKey(t *testing.T, url string) {
} }
// Get with caching // Get with caching
k2, err := OIDCJWK(thumb, url) pubkey, err = OIDCJWK(thumb, url)
if nil != err { if nil != err {
t.Fatal("Error fetching and caching key:", err) t.Fatal("[1] Error fetching and caching key:", err)
} }
// Look in cache for each (and succeed) // Look in cache for each (and succeed)
if pub := Get(thumb, ""); nil == pub { if pub := Get(thumb, ""); nil == pub {
t.Fatal("key was not properly cached by thumbprint", thumb) t.Fatal("key was not properly cached by thumbprint", thumb)
} }
if "" != k2.KeyID() {
if pub := Get(k2.KeyID(), url); nil == pub { // TODO thumb / id mapping
t.Fatal("key was not properly cached by kid", k2.KeyID()) thumb = keypairs.Thumbprint(pubkey)
if pub := Get(thumb, url); nil == pub {
t.Fatal("key was not properly cached by kid", pubkey)
} }
} else { // TODO
t.Log("Key did not have an explicit KeyID") /*
if 0 == len(keyfetch.GetID(thumb)) {
t.Log("Key did not have an explicit KeyID", thumb)
} }
*/
// Get again (should be sub-ms instant) // Get again (should be sub-ms instant)
now := time.Now() now := time.Now()
@ -65,7 +94,15 @@ func testCachesKey(t *testing.T, url string) {
} }
// Sanity check that the kid and thumb match // Sanity check that the kid and thumb match
if key.KeyID() != k2.KeyID() || key.Thumbprint() != k2.Thumbprint() { if !key.Equal(pubkey) || keypairs.Thumbprint(key) != keypairs.Thumbprint(pubkey) {
t.Fatal("SANITY: KeyIDs or Thumbprints do not match:", key.KeyID(), k2.KeyID(), key.Thumbprint(), k2.Thumbprint()) t.Fatalf("SANITY: [todo: KeyIDs or] Thumbprints do not match:\n%q != %q\n%q != %q",
keypairs.Thumbprint(key), keypairs.Thumbprint(pubkey),
keypairs.Thumbprint(key), keypairs.Thumbprint(pubkey))
}
// Get 404
_, err = PEM(url + "/will-not-be-found.xyz")
if nil == err {
t.Fatal("Should have an error when retrieving a 404 or index.html:", err)
} }
} }

200
keyfetch/issuer_test.go Normal file
View File

@ -0,0 +1,200 @@
package keyfetch
import (
"errors"
"net/http"
"net/url"
"testing"
)
func TestInvalidIssuer(t *testing.T) {
_, err := NewWhitelist([]string{"somethingorother"})
if nil == err {
t.Log("invalid http urls can get through, but that's okay")
}
_, err = NewWhitelist([]string{"//example.com/foo"})
if nil == err {
t.Fatal(errors.New("semi-bad url got through"))
}
}
func TestIssuerMatches(t *testing.T) {
// because [""] = strings.Split(os.Getenv("DOESNTEXIST"), ",")
trusted := []string{
"",
"https://example.com/",
"foobar.net/def/",
"https://*.wild.org",
"https://*.west.mali/verde",
}
privates := []string{
"http://happy.xyz/abc",
}
_, err := NewWhitelist(append(trusted, privates...))
if nil == err {
t.Fatal(errors.New("an insecure domain got through"))
}
// Empty list is allowed... I guess?
list, err := NewWhitelist(nil)
if nil != err {
t.Fatal(err)
}
// Combo list
list, err = NewWhitelist(trusted[1:], privates)
if nil != err {
t.Fatal(err)
}
var iss string
iss = "https://example.com"
if !list.IsTrustedIssuer(iss) {
t.Fatal("A good domain didn't make it:", iss)
}
iss = "https://example.com/"
if !list.IsTrustedIssuer(iss) {
t.Fatal("A good domain didn't make it:", iss)
}
iss = "http://example.com"
if list.IsTrustedIssuer(iss) {
t.Fatal("A bad URL slipped past", iss)
}
iss = "https://example.com/foo"
if list.IsTrustedIssuer(iss) {
t.Fatal("A bad URL slipped past", iss)
}
iss = "http://happy.xyz/abc"
if !list.IsTrustedIssuer(iss) {
t.Fatal("A good URL didn't make it:", iss)
}
iss = "http://happy.xyz/abc/"
if !list.IsTrustedIssuer(iss) {
t.Fatal("A good URL didn't make it:", iss)
}
iss = "http://happy.xyz/abc/d"
if list.IsTrustedIssuer(iss) {
t.Fatal("A bad URL slipped past", iss)
}
iss = "http://happy.xyz/abcd"
if list.IsTrustedIssuer(iss) {
t.Fatal("A bad URL slipped past", iss)
}
iss = "https://foobar.net/def"
if !list.IsTrustedIssuer(iss) {
t.Fatal("A good URL didn't make it:", iss)
}
iss = "https://foobar.net/def/"
if !list.IsTrustedIssuer(iss) {
t.Fatal("A good URL didn't make it:", iss)
}
iss = "http://foobar.net/def/"
if list.IsTrustedIssuer(iss) {
t.Fatal("A bad URL slipped past", iss)
}
iss = "https://foobar.net/def/e"
if list.IsTrustedIssuer(iss) {
t.Fatal("A bad URL slipped past", iss)
}
iss = "https://foobar.net/defe"
if list.IsTrustedIssuer(iss) {
t.Fatal("A bad URL slipped past", iss)
}
iss = "https://wild.org"
if list.IsTrustedIssuer(iss) {
t.Fatal("A bad URL slipped past", iss)
}
iss = "https://foo.wild.org"
if !list.IsTrustedIssuer(iss) {
t.Fatal("A good URL didn't make it:", iss)
}
iss = "https://sub.foo.wild.org"
if !list.IsTrustedIssuer(iss) {
t.Fatal("A good URL didn't make it:", iss)
}
iss = "https://foo.wild.org/cherries"
if list.IsTrustedIssuer(iss) {
t.Fatal("A bad URL slipped past", iss)
}
iss = "https://sub.west.mali/verde/"
if !list.IsTrustedIssuer(iss) {
t.Fatal("A good URL didn't make it:", iss)
}
iss = "https://sub.west.mali"
if list.IsTrustedIssuer(iss) {
t.Fatal("A bad URL slipped past", iss)
}
}
func TestImplicitIssuer(t *testing.T) {
var r *http.Request
var iss string
r = &http.Request{
Host: "example.com",
URL: &url.URL{Path: "/foo/bar/baz"},
Header: http.Header(map[string][]string{
"x-forwarded-host": []string{"example.com"},
}),
}
iss = "https://example.com/foo"
if !isTrustedIssuer(iss, nil, r) {
t.Fatal("A good URL didn't make it:", iss)
}
r = &http.Request{
Host: "example.com",
URL: &url.URL{Path: "/"},
Header: http.Header(map[string][]string{
"x-forwarded-host": []string{"example.com"},
"x-forwarded-proto": []string{"http"},
}),
}
iss = "http://example.com/foo"
if isTrustedIssuer(iss, nil, r) {
t.Fatal("A bad URL slipped past:", iss)
}
r = &http.Request{
Host: "example.com",
URL: &url.URL{Path: "/foo"},
Header: http.Header(map[string][]string{
"x-forwarded-host": []string{"example.com"},
}),
}
iss = "https://example.com/foo/bar/baz"
if isTrustedIssuer(iss, nil, r) {
t.Fatal("A bad URL slipped past:", iss)
}
r = &http.Request{
Host: "example.com",
URL: &url.URL{Path: "/"},
Header: http.Header(map[string][]string{
"x-forwarded-proto": []string{"https"},
}),
}
iss = "https://example.com/"
if !isTrustedIssuer(iss, nil, r) {
t.Fatal("A good URL didn't make it:", iss)
}
}

View File

@ -2,18 +2,32 @@
package uncached package uncached
import ( import (
"bytes"
"encoding/json" "encoding/json"
"crypto/rsa"
"crypto/ecdsa"
"errors"
"io" "io"
"io/ioutil"
"net" "net"
"net/http" "net/http"
"strings" "strings"
"time" "time"
keypairs "github.com/big-squid/go-keypairs" "git.rootprojects.org/root/keypairs"
) )
// URLishKey is TODO
var URLishKey = "_kid_url"
// JWKMapByID is TODO
type JWKMapByID = map[string]map[string]string
// PublicKeysMap is TODO
type PublicKeysMap = map[string]keypairs.PublicKeyDeprecated
// OIDCJWKs gets the OpenID Connect configuration from the baseURL and then calls JWKs with the specified jwks_uri // OIDCJWKs gets the OpenID Connect configuration from the baseURL and then calls JWKs with the specified jwks_uri
func OIDCJWKs(baseURL string) (map[string]map[string]string, map[string]keypairs.PublicKey, error) { func OIDCJWKs(baseURL string) (JWKMapByID, PublicKeysMap, error) {
baseURL = normalizeBaseURL(baseURL) baseURL = normalizeBaseURL(baseURL)
oidcConf := struct { oidcConf := struct {
JWKSURI string `json:"jwks_uri"` JWKSURI string `json:"jwks_uri"`
@ -34,16 +48,17 @@ func OIDCJWKs(baseURL string) (map[string]map[string]string, map[string]keypairs
} }
// WellKnownJWKs calls JWKs with baseURL + /.well-known/jwks.json as constructs the jwks_uri // WellKnownJWKs calls JWKs with baseURL + /.well-known/jwks.json as constructs the jwks_uri
func WellKnownJWKs(baseURL string) (map[string]map[string]string, map[string]keypairs.PublicKey, error) { func WellKnownJWKs(baseURL string) (JWKMapByID, PublicKeysMap, error) {
baseURL = normalizeBaseURL(baseURL) baseURL = normalizeBaseURL(baseURL)
url := baseURL + ".well-known/jwks.json"
return JWKs(baseURL + ".well-known/jwks.json") return JWKs(url)
} }
// JWKs fetches and parses a jwks.json (assuming well-known format) // JWKs fetches and parses a jwks.json (assuming well-known format)
func JWKs(jwksurl string) (map[string]map[string]string, map[string]keypairs.PublicKey, error) { func JWKs(jwksurl string) (JWKMapByID, PublicKeysMap, error) {
keys := map[string]keypairs.PublicKey{} keys := PublicKeysMap{}
maps := map[string]map[string]string{} maps := JWKMapByID{}
resp := struct { resp := struct {
Keys []map[string]interface{} `json:"keys"` Keys []map[string]interface{} `json:"keys"`
}{ }{
@ -62,19 +77,61 @@ func JWKs(jwksurl string) (map[string]map[string]string, map[string]keypairs.Pub
k := resp.Keys[i] k := resp.Keys[i]
m := getStringMap(k) m := getStringMap(k)
if key, err := keypairs.NewJWKPublicKey(m); nil != err { key, err := keypairs.NewJWKPublicKey(m)
if nil != err {
return nil, nil, err return nil, nil, err
} else {
keys[key.Thumbprint()] = key
maps[key.Thumbprint()] = m
} }
keys[keypairs.Thumbprint(key.Key())] = key
maps[keypairs.Thumbprint(key.Key())] = m
} }
return maps, keys, nil return maps, keys, nil
} }
// PEM fetches and parses a PEM (assuming well-known format)
func PEM(pemurl string) (map[string]string, keypairs.PublicKey, error) {
var pubd keypairs.PublicKeyDeprecated
if err := safeFetch(pemurl, func(body io.Reader) error {
pem, err := ioutil.ReadAll(body)
if nil != err {
return err
}
pubd, err = keypairs.ParsePublicKey(pem)
if nil != err {
return err
}
return nil
}); nil != err {
return nil, nil, err
}
jwk := map[string]interface{}{}
pub := pubd.Key()
body := bytes.NewBuffer(keypairs.MarshalJWKPublicKey(pub))
decoder := json.NewDecoder(body)
decoder.UseNumber()
_ = decoder.Decode(&jwk)
m := getStringMap(jwk)
m["kid"] = keypairs.Thumbprint(pub)
// TODO is this just junk?
m[URLishKey] = pemurl
switch pub.(type) {
case *ecdsa.PublicKey:
//p.KID = pemurl
case *rsa.PublicKey:
//p.KID = pemurl
default:
return nil, nil, errors.New("impossible key type")
}
return m, pub, nil
}
// Fetch retrieves a single JWK (plain, bare jwk) from a URL (off-spec) // Fetch retrieves a single JWK (plain, bare jwk) from a URL (off-spec)
func Fetch(url string) (map[string]string, keypairs.PublicKey, error) { func Fetch(url string) (map[string]string, keypairs.PublicKeyDeprecated, error) {
var m map[string]interface{} var m map[string]interface{}
if err := safeFetch(url, func(body io.Reader) error { if err := safeFetch(url, func(body io.Reader) error {
decoder := json.NewDecoder(body) decoder := json.NewDecoder(body)
@ -121,12 +178,15 @@ func safeFetch(url string, decoder decodeFunc) error {
}).Dial, }).Dial,
TLSHandshakeTimeout: 5 * time.Second, TLSHandshakeTimeout: 5 * time.Second,
} }
var netClient = &http.Client{ var client = &http.Client{
Timeout: time.Second * 10, Timeout: time.Second * 10,
Transport: netTransport, Transport: netTransport,
} }
res, err := netClient.Get(url) req, err := http.NewRequest("GET", url, nil)
req.Header.Set("User-Agent", "go-keypairs/keyfetch")
req.Header.Set("Accept", "application/json;q=0.9,*/*;q=0.8")
res, err := client.Do(req)
if nil != err { if nil != err {
return err return err
} }

View File

@ -6,7 +6,7 @@ import (
"errors" "errors"
"testing" "testing"
keypairs "github.com/big-squid/go-keypairs" "git.rootprojects.org/root/keypairs"
) )
func TestJWKs(t *testing.T) { func TestJWKs(t *testing.T) {

View File

@ -1,8 +1,8 @@
package keypairs package keypairs
import ( import (
"bytes"
"crypto" "crypto"
"crypto/dsa"
"crypto/ecdsa" "crypto/ecdsa"
"crypto/elliptic" "crypto/elliptic"
"crypto/rsa" "crypto/rsa"
@ -16,74 +16,146 @@ import (
"io" "io"
"log" "log"
"math/big" "math/big"
"strings"
"time" "time"
) )
var EInvalidPrivateKey = errors.New("PrivateKey must be of type *rsa.PrivateKey or *ecdsa.PrivateKey") // ErrInvalidPrivateKey means that the key is not a valid Private Key
var EInvalidPublicKey = errors.New("PublicKey must be of type *rsa.PublicKey or *ecdsa.PublicKey") var ErrInvalidPrivateKey = errors.New("PrivateKey must be of type *rsa.PrivateKey or *ecdsa.PrivateKey")
var EParsePrivateKey = errors.New("PrivateKey bytes could not be parsed as PEM or DER (PKCS8, SEC1, or PKCS1) or JWK")
var EParseJWK = errors.New("JWK is missing required base64-encoded JSON fields")
var EInvalidKeyType = errors.New("The JWK's 'kty' must be either 'RSA' or 'EC'")
var EInvalidCurve = errors.New("The JWK's 'crv' must be either of the NIST standards 'P-256' or 'P-384'")
const EDevSwapPrivatePublic = "[Developer Error] You passed either crypto.PrivateKey or crypto.PublicKey where the other was expected." // ErrInvalidPublicKey means that the key is not a valid Public Key
var ErrInvalidPublicKey = errors.New("PublicKey must be of type *rsa.PublicKey or *ecdsa.PublicKey")
const EDevBadKeyType = "[Developer Error] crypto.PublicKey and crypto.PrivateKey are somewhat deceptive. They're actually empty interfaces that accept any object, even non-crypto objects. You passed an object of type '%T' by mistake." // ErrParsePublicKey means that the bytes cannot be parsed in any known format
var ErrParsePublicKey = errors.New("PublicKey bytes could not be parsed as PEM or DER (PKIX/SPKI, PKCS1, or X509 Certificate) or JWK")
// ErrParsePrivateKey means that the bytes cannot be parsed in any known format
var ErrParsePrivateKey = errors.New("PrivateKey bytes could not be parsed as PEM or DER (PKCS8, SEC1, or PKCS1) or JWK")
// ErrParseJWK means that the JWK is valid JSON but not a valid JWK
var ErrParseJWK = errors.New("JWK is missing required base64-encoded JSON fields")
// ErrInvalidKeyType means that the key is not an acceptable type
var ErrInvalidKeyType = errors.New("The JWK's 'kty' must be either 'RSA' or 'EC'")
// ErrInvalidCurve means that a non-standard curve was used
var ErrInvalidCurve = errors.New("The JWK's 'crv' must be either of the NIST standards 'P-256' or 'P-384'")
// ErrUnexpectedPublicKey means that a Private Key was expected
var ErrUnexpectedPublicKey = errors.New("PrivateKey was given where PublicKey was expected")
// ErrUnexpectedPrivateKey means that a Public Key was expected
var ErrUnexpectedPrivateKey = errors.New("PublicKey was given where PrivateKey was expected")
// ErrDevSwapPrivatePublic means that the developer compiled bad code that swapped public and private keys
const ErrDevSwapPrivatePublic = "[Developer Error] You passed either crypto.PrivateKey or crypto.PublicKey where the other was expected."
// ErrDevBadKeyType means that the developer compiled bad code that passes the wrong type
const ErrDevBadKeyType = "[Developer Error] crypto.PublicKey and crypto.PrivateKey are somewhat deceptive. They're actually empty interfaces that accept any object, even non-crypto objects. You passed an object of type '%T' by mistake."
// PrivateKey is a zero-cost typesafe substitue for crypto.PrivateKey // PrivateKey is a zero-cost typesafe substitue for crypto.PrivateKey
type PrivateKey interface { type PrivateKey interface {
Public() crypto.PublicKey Public() crypto.PublicKey
Equal(x crypto.PrivateKey) bool
} }
// PublicKey thinly veils crypto.PublicKey for type safety // PublicKey is so that v0.7.x can use golang v1.15 keys
type PublicKey interface { type PublicKey interface {
crypto.PublicKey Equal(x crypto.PublicKey) bool
Thumbprint() string
KeyID() string
Key() crypto.PublicKey
} }
// PublicKeyDeprecated thinly veils crypto.PublicKey for type safety
type PublicKeyDeprecated interface {
crypto.PublicKey
//Equal(x crypto.PublicKey) bool
//Thumbprint() string
KeyID() string
Key() PublicKey
ExpiresAt() time.Time
}
// ECPublicKey adds common methods to *ecdsa.PublicKey for type safety
type ECPublicKey struct { type ECPublicKey struct {
PublicKey *ecdsa.PublicKey // empty interface PublicKey *ecdsa.PublicKey // empty interface
KID string KID string
Expiry time.Time Expiry time.Time
} }
// RSAPublicKey adds common methods to *rsa.PublicKey for type safety
type RSAPublicKey struct { type RSAPublicKey struct {
PublicKey *rsa.PublicKey // empty interface PublicKey *rsa.PublicKey // empty interface
KID string KID string
Expiry time.Time Expiry time.Time
} }
// Thumbprint returns a JWK thumbprint. See https://stackoverflow.com/questions/42588786/how-to-fingerprint-a-jwk
func (p *ECPublicKey) Thumbprint() string { func (p *ECPublicKey) Thumbprint() string {
return ThumbprintUntypedPublicKey(p.PublicKey) return ThumbprintUntypedPublicKey(p.PublicKey)
} }
// Equal returns true if the public key is equal.
func (p *ECPublicKey) Equal(x crypto.PublicKey) bool {
return p.PublicKey.Equal(x)
}
// KeyID returns the JWK `kid`, which will be the Thumbprint for keys generated with this library
func (p *ECPublicKey) KeyID() string { func (p *ECPublicKey) KeyID() string {
return p.KID return p.KID
} }
func (p *ECPublicKey) Key() crypto.PublicKey {
// Key returns the PublicKey
func (p *ECPublicKey) Key() PublicKey {
return p.PublicKey return p.PublicKey
} }
// ExpireAt sets the time at which this Public Key should be considered invalid
func (p *ECPublicKey) ExpireAt(t time.Time) { func (p *ECPublicKey) ExpireAt(t time.Time) {
p.Expiry = t p.Expiry = t
} }
// ExpiresAt gets the time at which this Public Key should be considered invalid
func (p *ECPublicKey) ExpiresAt() time.Time {
return p.Expiry
}
// Thumbprint returns a JWK thumbprint. See https://stackoverflow.com/questions/42588786/how-to-fingerprint-a-jwk
func (p *RSAPublicKey) Thumbprint() string { func (p *RSAPublicKey) Thumbprint() string {
return ThumbprintUntypedPublicKey(p.PublicKey) return ThumbprintUntypedPublicKey(p.PublicKey)
} }
// Equal returns true if the public key is equal.
func (p *RSAPublicKey) Equal(x crypto.PublicKey) bool {
return p.PublicKey.Equal(x)
}
// KeyID returns the JWK `kid`, which will be the Thumbprint for keys generated with this library
func (p *RSAPublicKey) KeyID() string { func (p *RSAPublicKey) KeyID() string {
return p.KID return p.KID
} }
func (p *RSAPublicKey) Key() crypto.PublicKey {
// Key returns the PublicKey
func (p *RSAPublicKey) Key() PublicKey {
return p.PublicKey return p.PublicKey
} }
// ExpireAt sets the time at which this Public Key should be considered invalid
func (p *RSAPublicKey) ExpireAt(t time.Time) { func (p *RSAPublicKey) ExpireAt(t time.Time) {
p.Expiry = t p.Expiry = t
} }
// ExpiresAt gets the time at which this Public Key should be considered invalid
func (p *RSAPublicKey) ExpiresAt() time.Time {
return p.Expiry
}
// NewPublicKey wraps a crypto.PublicKey to make it typesafe. // NewPublicKey wraps a crypto.PublicKey to make it typesafe.
func NewPublicKey(pub crypto.PublicKey, kid ...string) PublicKey { func NewPublicKey(pub crypto.PublicKey, kid ...string) PublicKeyDeprecated {
var k PublicKey _, ok := pub.(PublicKey)
if !ok {
panic("Developer Error: not a crypto.PublicKey")
}
var k PublicKeyDeprecated
switch p := pub.(type) { switch p := pub.(type) {
case *ecdsa.PublicKey: case *ecdsa.PublicKey:
eckey := &ECPublicKey{ eckey := &ECPublicKey{
@ -105,52 +177,55 @@ func NewPublicKey(pub crypto.PublicKey, kid ...string) PublicKey {
rsakey.KID = ThumbprintRSAPublicKey(p) rsakey.KID = ThumbprintRSAPublicKey(p)
} }
k = rsakey k = rsakey
case *ecdsa.PrivateKey:
panic(errors.New(EDevSwapPrivatePublic))
case *rsa.PrivateKey:
panic(errors.New(EDevSwapPrivatePublic))
case *dsa.PublicKey:
panic(EInvalidPublicKey)
case *dsa.PrivateKey:
panic(EInvalidPublicKey)
default: default:
panic(errors.New(fmt.Sprintf(EDevBadKeyType, pub))) panic(fmt.Errorf(ErrDevBadKeyType, pub))
} }
return k return k
} }
// MarshalJWKPublicKey outputs a JWK with its key id (kid) and an optional expiration,
// making it suitable for use as an OIDC public key.
func MarshalJWKPublicKey(key PublicKey, exp ...time.Time) []byte { func MarshalJWKPublicKey(key PublicKey, exp ...time.Time) []byte {
// thumbprint keys are alphabetically sorted and only include the necessary public parts // thumbprint keys are alphabetically sorted and only include the necessary public parts
switch k := key.Key().(type) { switch k := key.(type) {
case *rsa.PublicKey: case *rsa.PublicKey:
return MarshalRSAPublicKey(k, exp...) return MarshalRSAPublicKey(k, exp...)
case *ecdsa.PublicKey: case *ecdsa.PublicKey:
return MarshalECPublicKey(k, exp...) return MarshalECPublicKey(k, exp...)
case *dsa.PublicKey:
panic(EInvalidPublicKey)
default: default:
// this is unreachable because we know the types that we pass in // this is unreachable because we know the types that we pass in
log.Printf("keytype: %t, %+v\n", key, key) log.Printf("keytype: %t, %+v\n", key, key)
panic(EInvalidPublicKey) panic(ErrInvalidPublicKey)
} }
} }
func ThumbprintPublicKey(pub *PublicKey) string { // Thumbprint returns the SHA256 RFC-spec JWK thumbprint
func Thumbprint(pub PublicKey) string {
return ThumbprintUntypedPublicKey(pub) return ThumbprintUntypedPublicKey(pub)
} }
// ThumbprintPublicKey returns the SHA256 RFC-spec JWK thumbprint
func ThumbprintPublicKey(pub PublicKeyDeprecated) string {
return ThumbprintUntypedPublicKey(pub.Key())
}
// ThumbprintUntypedPublicKey is a non-typesafe version of ThumbprintPublicKey
// (but will still panic, to help you discover bugs in development rather than production).
func ThumbprintUntypedPublicKey(pub crypto.PublicKey) string { func ThumbprintUntypedPublicKey(pub crypto.PublicKey) string {
switch p := pub.(type) { switch p := pub.(type) {
case PublicKeyDeprecated:
return ThumbprintUntypedPublicKey(p.Key())
case *ecdsa.PublicKey: case *ecdsa.PublicKey:
return ThumbprintECPublicKey(p) return ThumbprintECPublicKey(p)
case *rsa.PublicKey: case *rsa.PublicKey:
return ThumbprintRSAPublicKey(p) return ThumbprintRSAPublicKey(p)
default: default:
panic(EInvalidPublicKey) panic(ErrInvalidPublicKey)
} }
} }
// MarshalECPublicKey will take an EC key and output a JWK, with optional expiration date
func MarshalECPublicKey(k *ecdsa.PublicKey, exp ...time.Time) []byte { func MarshalECPublicKey(k *ecdsa.PublicKey, exp ...time.Time) []byte {
thumb := ThumbprintECPublicKey(k) thumb := ThumbprintECPublicKey(k)
crv := k.Curve.Params().Name crv := k.Curve.Params().Name
@ -163,6 +238,7 @@ func MarshalECPublicKey(k *ecdsa.PublicKey, exp ...time.Time) []byte {
return []byte(fmt.Sprintf(`{"kid":%q,"use":"sig",%s"crv":%q,"kty":"EC","x":%q,"y":%q}`, thumb, expstr, crv, x, y)) return []byte(fmt.Sprintf(`{"kid":%q,"use":"sig",%s"crv":%q,"kty":"EC","x":%q,"y":%q}`, thumb, expstr, crv, x, y))
} }
// MarshalECPublicKeyWithoutKeyID will output the most minimal version of an EC JWK (no key id, no "use" flag, nada)
func MarshalECPublicKeyWithoutKeyID(k *ecdsa.PublicKey) []byte { func MarshalECPublicKeyWithoutKeyID(k *ecdsa.PublicKey) []byte {
crv := k.Curve.Params().Name crv := k.Curve.Params().Name
x := base64.RawURLEncoding.EncodeToString(k.X.Bytes()) x := base64.RawURLEncoding.EncodeToString(k.X.Bytes())
@ -170,12 +246,14 @@ func MarshalECPublicKeyWithoutKeyID(k *ecdsa.PublicKey) []byte {
return []byte(fmt.Sprintf(`{"crv":%q,"kty":"EC","x":%q,"y":%q}`, crv, x, y)) return []byte(fmt.Sprintf(`{"crv":%q,"kty":"EC","x":%q,"y":%q}`, crv, x, y))
} }
// ThumbprintECPublicKey will output a RFC-spec SHA256 JWK thumbprint of an EC public key
func ThumbprintECPublicKey(k *ecdsa.PublicKey) string { func ThumbprintECPublicKey(k *ecdsa.PublicKey) string {
thumbprintable := MarshalECPublicKeyWithoutKeyID(k) thumbprintable := MarshalECPublicKeyWithoutKeyID(k)
sha := sha256.Sum256(thumbprintable) sha := sha256.Sum256(thumbprintable)
return base64.RawURLEncoding.EncodeToString(sha[:]) return base64.RawURLEncoding.EncodeToString(sha[:])
} }
// MarshalRSAPublicKey will take an RSA key and output a JWK, with optional expiration date
func MarshalRSAPublicKey(p *rsa.PublicKey, exp ...time.Time) []byte { func MarshalRSAPublicKey(p *rsa.PublicKey, exp ...time.Time) []byte {
thumb := ThumbprintRSAPublicKey(p) thumb := ThumbprintRSAPublicKey(p)
e := base64.RawURLEncoding.EncodeToString(big.NewInt(int64(p.E)).Bytes()) e := base64.RawURLEncoding.EncodeToString(big.NewInt(int64(p.E)).Bytes())
@ -187,51 +265,49 @@ func MarshalRSAPublicKey(p *rsa.PublicKey, exp ...time.Time) []byte {
return []byte(fmt.Sprintf(`{"kid":%q,"use":"sig",%s"e":%q,"kty":"RSA","n":%q}`, thumb, expstr, e, n)) return []byte(fmt.Sprintf(`{"kid":%q,"use":"sig",%s"e":%q,"kty":"RSA","n":%q}`, thumb, expstr, e, n))
} }
// MarshalRSAPublicKeyWithoutKeyID will output the most minimal version of an RSA JWK (no key id, no "use" flag, nada)
func MarshalRSAPublicKeyWithoutKeyID(p *rsa.PublicKey) []byte { func MarshalRSAPublicKeyWithoutKeyID(p *rsa.PublicKey) []byte {
e := base64.RawURLEncoding.EncodeToString(big.NewInt(int64(p.E)).Bytes()) e := base64.RawURLEncoding.EncodeToString(big.NewInt(int64(p.E)).Bytes())
n := base64.RawURLEncoding.EncodeToString(p.N.Bytes()) n := base64.RawURLEncoding.EncodeToString(p.N.Bytes())
return []byte(fmt.Sprintf(`{"e":%q,"kty":"RSA","n":%q}`, e, n)) return []byte(fmt.Sprintf(`{"e":%q,"kty":"RSA","n":%q}`, e, n))
} }
// ThumbprintRSAPublicKey will output a RFC-spec SHA256 JWK thumbprint of an EC public key
func ThumbprintRSAPublicKey(p *rsa.PublicKey) string { func ThumbprintRSAPublicKey(p *rsa.PublicKey) string {
thumbprintable := MarshalRSAPublicKeyWithoutKeyID(p) thumbprintable := MarshalRSAPublicKeyWithoutKeyID(p)
sha := sha256.Sum256([]byte(thumbprintable)) sha := sha256.Sum256([]byte(thumbprintable))
return base64.RawURLEncoding.EncodeToString(sha[:]) return base64.RawURLEncoding.EncodeToString(sha[:])
} }
// ParsePrivateKey will try to parse the bytes you give it
// in any of the supported formats: PEM, DER, PKCS8, PKCS1, SEC1, and JWK
func ParsePrivateKey(block []byte) (PrivateKey, error) { func ParsePrivateKey(block []byte) (PrivateKey, error) {
var pemblock *pem.Block blocks, err := getPEMBytes(block)
var blocks = make([][]byte, 0, 1) if nil != err {
return nil, ErrParsePrivateKey
// Parse the PEM, if it's a pem
for {
pemblock, block = pem.Decode(block)
if nil != pemblock {
// got one block, there may be more
blocks = append(blocks, pemblock.Bytes)
} else {
// the last block was not a PEM block
// therefore the next isn't either
if 0 != len(block) {
blocks = append(blocks, block)
}
break
}
} }
// Parse PEM blocks (openssl generates junk metadata blocks for ECs) // Parse PEM blocks (openssl generates junk metadata blocks for ECs)
// or the original DER, or the JWK // or the original DER, or the JWK
for i, _ := range blocks { for i := range blocks {
block = blocks[i] block = blocks[i]
if key, err := parsePrivateKey(block); nil == err { if key, err := parsePrivateKey(block); nil == err {
return key, nil return key, nil
} }
} }
for i := range blocks {
block = blocks[i]
if _, err := parsePublicKey(block); nil == err {
return nil, ErrUnexpectedPublicKey
}
}
// If we didn't parse a key arleady, we failed // If we didn't parse a key arleady, we failed
return nil, EParsePrivateKey return nil, ErrParsePrivateKey
} }
// ParsePrivateKeyString calls ParsePrivateKey([]byte(key)) for all you lazy folk.
func ParsePrivateKeyString(block string) (PrivateKey, error) { func ParsePrivateKeyString(block string) (PrivateKey, error) {
return ParsePrivateKey([]byte(block)) return ParsePrivateKey([]byte(block))
} }
@ -248,7 +324,7 @@ func parsePrivateKey(der []byte) (PrivateKey, error) {
case *ecdsa.PrivateKey: case *ecdsa.PrivateKey:
key = k key = k
default: default:
// ignore nil and unknown key types err = errors.New("Only RSA and ECDSA (EC) Private Keys are supported")
} }
} }
@ -275,40 +351,158 @@ func parsePrivateKey(der []byte) (PrivateKey, error) {
return key, nil return key, nil
} }
func NewJWKPublicKey(m map[string]string) (PublicKey, error) { func getPEMBytes(block []byte) ([][]byte, error) {
var pemblock *pem.Block
var blocks = make([][]byte, 0, 1)
// Parse the PEM, if it's a pem
for {
pemblock, block = pem.Decode(block)
if nil != pemblock {
// got one block, there may be more
blocks = append(blocks, pemblock.Bytes)
} else {
// the last block was not a PEM block
// therefore the next isn't either
if 0 != len(block) {
blocks = append(blocks, block)
}
break
}
}
if len(blocks) > 0 {
return blocks, nil
}
return nil, errors.New("no PEM blocks found")
}
// ParsePublicKey will try to parse the bytes you give it
// in any of the supported formats: PEM, DER, PKIX/SPKI, PKCS1, x509 Certificate, and JWK
func ParsePublicKey(block []byte) (PublicKeyDeprecated, error) {
blocks, err := getPEMBytes(block)
if nil != err {
return nil, ErrParsePublicKey
}
// Parse PEM blocks (openssl generates junk metadata blocks for ECs)
// or the original DER, or the JWK
for i := range blocks {
block = blocks[i]
if key, err := parsePublicKey(block); nil == err {
return key, nil
}
}
for i := range blocks {
block = blocks[i]
if _, err := parsePrivateKey(block); nil == err {
return nil, ErrUnexpectedPrivateKey
}
}
// If we didn't parse a key arleady, we failed
return nil, ErrParsePublicKey
}
// ParsePublicKeyString calls ParsePublicKey([]byte(key)) for all you lazy folk.
func ParsePublicKeyString(block string) (PublicKeyDeprecated, error) {
return ParsePublicKey([]byte(block))
}
func parsePublicKey(der []byte) (PublicKeyDeprecated, error) {
cert, err := x509.ParseCertificate(der)
if nil == err {
switch k := cert.PublicKey.(type) {
case *rsa.PublicKey:
return NewPublicKey(k), nil
case *ecdsa.PublicKey:
return NewPublicKey(k), nil
default:
return nil, errors.New("Only RSA and ECDSA (EC) Public Keys are supported")
}
}
//fmt.Println("1. ParsePKIXPublicKey")
xkey, err := x509.ParsePKIXPublicKey(der)
if nil == err {
switch k := xkey.(type) {
case *rsa.PublicKey:
return NewPublicKey(k), nil
case *ecdsa.PublicKey:
return NewPublicKey(k), nil
default:
return nil, errors.New("Only RSA and ECDSA (EC) Public Keys are supported")
}
}
//fmt.Println("3. ParsePKCS1PrublicKey")
rkey, err := x509.ParsePKCS1PublicKey(der)
if nil == err {
//fmt.Println("4. ParseJWKPublicKey")
return NewPublicKey(rkey), nil
}
return ParseJWKPublicKey(der)
/*
// But did you know?
// You must return nil explicitly for interfaces
// https://golang.org/doc/faq#nil_error
if nil != err {
return nil, err
}
*/
}
// NewJWKPublicKey contstructs a PublicKey from the relevant pieces a map[string]string (generic JSON)
func NewJWKPublicKey(m map[string]string) (PublicKeyDeprecated, error) {
switch m["kty"] { switch m["kty"] {
case "RSA": case "RSA":
return parseRSAPublicKey(m) return parseRSAPublicKey(m)
case "EC": case "EC":
return parseECPublicKey(m) return parseECPublicKey(m)
default: default:
return nil, EInvalidKeyType return nil, ErrInvalidKeyType
} }
} }
func ParseJWKPublicKey(b []byte) (PublicKey, error) { // ParseJWKPublicKey parses a JSON-encoded JWK and returns a PublicKey, or a (hopefully) helpful error message
func ParseJWKPublicKey(b []byte) (PublicKeyDeprecated, error) {
// RSA and EC have "d" as a private part
if bytes.Contains(b, []byte(`"d"`)) {
return nil, ErrUnexpectedPrivateKey
}
return newJWKPublicKey(b) return newJWKPublicKey(b)
} }
func ParseJWKPublicKeyString(s string) (PublicKey, error) { // ParseJWKPublicKeyString calls ParseJWKPublicKey([]byte(key)) for all you lazy folk.
func ParseJWKPublicKeyString(s string) (PublicKeyDeprecated, error) {
if strings.Contains(s, `"d"`) {
return nil, ErrUnexpectedPrivateKey
}
return newJWKPublicKey(s) return newJWKPublicKey(s)
} }
func DecodeJWKPublicKey(r io.Reader) (PublicKey, error) { // DecodeJWKPublicKey stream-decodes a JSON-encoded JWK and returns a PublicKey, or a (hopefully) helpful error message
return newJWKPublicKey(r) func DecodeJWKPublicKey(r io.Reader) (PublicKeyDeprecated, error) {
m := make(map[string]string)
if err := json.NewDecoder(r).Decode(&m); nil != err {
return nil, err
}
if d := m["d"]; "" != d {
return nil, ErrUnexpectedPrivateKey
}
return newJWKPublicKey(m)
} }
func newJWKPublicKey(data interface{}) (PublicKey, error) { // the underpinnings of the parser as used by the typesafe wrappers
func newJWKPublicKey(data interface{}) (PublicKeyDeprecated, error) {
var m map[string]string var m map[string]string
switch d := data.(type) { switch d := data.(type) {
case map[string]string: case map[string]string:
m = d m = d
case io.Reader:
m = make(map[string]string)
if err := json.NewDecoder(d).Decode(&m); nil != err {
return nil, err
}
case string: case string:
if err := json.Unmarshal([]byte(d), &m); nil != err { if err := json.Unmarshal([]byte(d), &m); nil != err {
return nil, err return nil, err
@ -324,6 +518,7 @@ func newJWKPublicKey(data interface{}) (PublicKey, error) {
return NewJWKPublicKey(m) return NewJWKPublicKey(m)
} }
// ParseJWKPrivateKey parses a JSON-encoded JWK and returns a PrivateKey, or a (hopefully) helpful error message
func ParseJWKPrivateKey(b []byte) (PrivateKey, error) { func ParseJWKPrivateKey(b []byte) (PrivateKey, error) {
var m map[string]string var m map[string]string
if err := json.Unmarshal(b, &m); nil != err { if err := json.Unmarshal(b, &m); nil != err {
@ -336,7 +531,7 @@ func ParseJWKPrivateKey(b []byte) (PrivateKey, error) {
case "EC": case "EC":
return parseECPrivateKey(m) return parseECPrivateKey(m)
default: default:
return nil, EInvalidKeyType return nil, ErrInvalidKeyType
} }
} }
@ -346,7 +541,7 @@ func parseRSAPublicKey(m map[string]string) (*RSAPublicKey, error) {
n, _ := base64.RawURLEncoding.DecodeString(m["n"]) n, _ := base64.RawURLEncoding.DecodeString(m["n"])
e, _ := base64.RawURLEncoding.DecodeString(m["e"]) e, _ := base64.RawURLEncoding.DecodeString(m["e"])
if 0 == len(n) || 0 == len(e) { if 0 == len(n) || 0 == len(e) {
return nil, EParseJWK return nil, ErrParseJWK
} }
ni := &big.Int{} ni := &big.Int{}
ni.SetBytes(n) ni.SetBytes(n)
@ -377,7 +572,7 @@ func parseRSAPrivateKey(m map[string]string) (key *rsa.PrivateKey, err error) {
dq, _ := base64.RawURLEncoding.DecodeString(m["dq"]) dq, _ := base64.RawURLEncoding.DecodeString(m["dq"])
qinv, _ := base64.RawURLEncoding.DecodeString(m["qi"]) qinv, _ := base64.RawURLEncoding.DecodeString(m["qi"])
if 0 == len(d) || 0 == len(p) || 0 == len(dp) || 0 == len(dq) || 0 == len(qinv) { if 0 == len(d) || 0 == len(p) || 0 == len(dp) || 0 == len(dq) || 0 == len(qinv) {
return nil, EParseJWK return nil, ErrParseJWK
} }
di := &big.Int{} di := &big.Int{}
@ -413,7 +608,7 @@ func parseECPublicKey(m map[string]string) (*ECPublicKey, error) {
x, _ := base64.RawURLEncoding.DecodeString(m["x"]) x, _ := base64.RawURLEncoding.DecodeString(m["x"])
y, _ := base64.RawURLEncoding.DecodeString(m["y"]) y, _ := base64.RawURLEncoding.DecodeString(m["y"])
if 0 == len(x) || 0 == len(y) || 0 == len(m["crv"]) { if 0 == len(x) || 0 == len(y) || 0 == len(m["crv"]) {
return nil, EParseJWK return nil, ErrParseJWK
} }
xi := &big.Int{} xi := &big.Int{}
@ -431,7 +626,7 @@ func parseECPublicKey(m map[string]string) (*ECPublicKey, error) {
case "P-521": case "P-521":
crv = elliptic.P521() crv = elliptic.P521()
default: default:
return nil, EInvalidCurve return nil, ErrInvalidCurve
} }
pub := &ecdsa.PublicKey{ pub := &ecdsa.PublicKey{
@ -454,7 +649,7 @@ func parseECPrivateKey(m map[string]string) (*ecdsa.PrivateKey, error) {
d, _ := base64.RawURLEncoding.DecodeString(m["d"]) d, _ := base64.RawURLEncoding.DecodeString(m["d"])
if 0 == len(d) { if 0 == len(d) {
return nil, EParseJWK return nil, ErrParseJWK
} }
di := &big.Int{} di := &big.Int{}
di.SetBytes(d) di.SetBytes(d)

View File

@ -4,6 +4,8 @@ import (
"crypto/ecdsa" "crypto/ecdsa"
"crypto/rsa" "crypto/rsa"
"io/ioutil" "io/ioutil"
"log"
"net/http"
"testing" "testing"
) )
@ -41,6 +43,53 @@ func TestParsePrivateKeyEC(t *testing.T) {
} }
} }
func TestParseUnexpectedPrivateKey(t *testing.T) {
keypaths := []string{
"fixtures/privkey-ec-p256.jwk.json",
"fixtures/privkey-ec-p256.sec1.pem",
"fixtures/privkey-ec-p256.pkcs8.pem",
"fixtures/privkey-rsa-2048.jwk.json",
"fixtures/privkey-rsa-2048.pkcs1.pem",
"fixtures/privkey-rsa-2048.pkcs8.pem",
}
for i := range keypaths {
path := keypaths[i]
b, err := ioutil.ReadFile(path)
if nil != err {
t.Fatal(path, err)
}
_, err = ParsePublicKey(b)
switch err {
case ErrUnexpectedPrivateKey:
continue
default:
t.Fatal(path, err)
}
}
}
func TestParseUnexpectedPublicKey(t *testing.T) {
keypaths := []string{
"fixtures/pub-ec-p256.jwk.json",
}
for i := range keypaths {
path := keypaths[i]
b, err := ioutil.ReadFile(path)
if nil != err {
t.Fatal(path, err)
}
_, err = ParsePrivateKey(b)
switch err {
case ErrUnexpectedPublicKey:
continue
default:
t.Fatal(path, err)
}
}
}
func TestParsePrivateKeyRSA(t *testing.T) { func TestParsePrivateKeyRSA(t *testing.T) {
keypaths := []string{ keypaths := []string{
"fixtures/privkey-rsa-2048.jwk.json", "fixtures/privkey-rsa-2048.jwk.json",
@ -67,3 +116,19 @@ func TestParsePrivateKeyRSA(t *testing.T) {
} }
} }
} }
func TestParseCertificate(t *testing.T) {
resp, err := http.Get("https://example.auth0.com/pem")
if nil != err {
log.Fatal(err)
}
bytes, err := ioutil.ReadAll(resp.Body)
if nil != err {
log.Fatal(err)
}
_, err = ParsePublicKey(bytes)
if nil != err {
log.Fatal("Could not parse PEM/cert from auth0")
log.Fatal(err)
}
}

41
keyserve/doc.go Normal file
View File

@ -0,0 +1,41 @@
/*
Package keyserve provides middleware to serve Public Keys
via OIDC-style (https://example.com/.well-known/openid-configuration)
and Auth0-style (https://example.com/.well-known/jwks.json)
URLs. It uses the keypairs package to encode to JWK format.
Basic Usage
import (
"crypto/ecdsa"
"crypto/rand"
"time"
"git.rootprojects.org/root/keypairs/keyserve"
)
key, _ := ecdsa.GenerateKey(elliptic.P256, rand.Reader)
pub := key.Public()
handlers := &keyserve.Middleware{
// the self-reference used for building the openid-configuration url
BaseURL: "https://example.com/",
// public keys used to verify token signatures
Keys: []keypairs.PublicKey{ keypairs.NewPublicKey(pub) }
// how long clients should cache your public key
ExpiresIn: 72 * time.Hour
}
You can then use the handlers anywhere http.HandleFunc is allowed:
http.HandleFunc(keyserve.PEMPath, handlers.Auth0PEM)
http.HandleFunc(keyserve.JWKsPath, handlers.WellKnownJWKs)
http.HandleFunc(keyserve.OIDCPath, handlers.WellKnownOIDC)
*/
package keyserve

172
keyserve/keyserve.go Normal file
View File

@ -0,0 +1,172 @@
package keyserve
import (
"crypto/ecdsa"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"git.rootprojects.org/root/keypairs"
)
// DefaultExpiresIn is 3 days
var DefaultExpiresIn = 72 * time.Hour
// JWKsPath is "/.well-known/jwks.json" (Auth0 spec)
const JWKsPath = "/.well-known/jwks.json"
var jwksURL, _ = url.Parse(".well-known/jwks.json")
// OIDCPath is "/.well-known/openid-configuration" (OIDC spec)
const OIDCPath = "/.well-known/openid-configuration"
var oidcURL, _ = url.Parse(".well-known/openid-configuration")
// PEMPath is "/pem" (Auth0 convention)
const PEMPath = "/pem"
var auth0PEMURL, _ = url.Parse("pem")
// for convenience
var notime time.Duration
var never = time.Time{}
// Middleware holds your public keys and has http handler methods for OIDC and Auth0 JWKs
type Middleware struct {
BaseURL *url.URL
Keys []keypairs.PublicKeyDeprecated
ExpiresIn time.Duration
}
// General Note:
// Some frameworks don't properly handle the trailing ;charset=utf-8
// for Content-Type, and it doesn't add practical benefit, so we omit it
// (JSON _is_ utf-8, per spec, already).
// Handler will match either OIDC or Auth0 jwks URLs and return true if it
// matches on (and responds to) either. Otherwise it will return false.
func (m *Middleware) Handler(w http.ResponseWriter, r *http.Request) bool {
if strings.HasSuffix(r.URL.Path, JWKsPath) {
m.WellKnownJWKs(w, r)
return true
}
if strings.HasSuffix(r.URL.Path, OIDCPath) {
m.WellKnownOIDC(w, r)
return true
}
if strings.HasSuffix(r.URL.Path, PEMPath) {
m.Auth0PEM(w, r)
return true
}
return false
}
// WellKnownOIDC serves a minimal OIDC config for the purpose of distributing
// JWKs if you need something more powerful, do it yourself.
// (but feel free to copy the code here)
//
// Security Note: If you do not supply Middleware.BaseURL, it will be taken
// from r.Host (since Web Browsers will always present it as the domain being
// accessed, which is not the case with TLS.ServerName over HTTP/2).
// This is normally not a problem because an attacker can only spoof back to
// themselves the jwks_uri. HOWEVER (DANGER, DANGER WILL ROBINSON) - RED FLAG -
// somewhere in the universe there is surely some old janky podunk proxy, still
// in use today, which is vulnerable to basic cache poisening which could cause
// others to receive a cached version of the malicious response rather than
// hitting the server and getting the correct response. Unlikely that that's
// you (and if it is you have much bigger problems), but I feel the need to
// warn you all the same - so just be sure to specify BaseURL.
func (m *Middleware) WellKnownOIDC(w http.ResponseWriter, r *http.Request) {
var baseURL url.URL
// Use a defined BaseURL, or an implicit one
if nil != m.BaseURL {
baseURL = *m.BaseURL
} else {
baseURL = *r.URL
if nil == r.TLS && "https" != r.Header.Get("X-Forwarded-Proto") {
baseURL.Scheme = "http"
} else {
baseURL.Scheme = "https"
}
baseURL.Host = r.Host
baseURL.Path = strings.TrimSuffix(baseURL.Path, oidcURL.Path)
}
// avoiding with correctly handling trailing vs non-trailing '/'
u := baseURL.ResolveReference(jwksURL)
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(fmt.Sprintf(`{ "issuer": %q, "jwks_uri": %q }`, baseURL.String(), u.String())))
}
// WellKnownJWKs serves a JSON array of keys, no fluff
func (m *Middleware) WellKnownJWKs(w http.ResponseWriter, r *http.Request) {
// Use either the user-supplied key expiration or our own default
s := m.ExpiresIn
if notime == s {
s = DefaultExpiresIn
}
exp := time.Now().Add(s)
jwks := marshalJWKs(m.Keys, exp)
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(fmt.Sprintf(`{"keys":[%s]}`, strings.Join(jwks, ","))))
}
// Auth0PEM serves a PEM containing a public key
func (m *Middleware) Auth0PEM(w http.ResponseWriter, r *http.Request) {
// TODO serve a self-signed root certificate (like Auth0),
// with a proper expiration date, instead
w.Header().Set("Content-Type", "application/x-pem-file")
switch pub := m.Keys[0].Key().(type) {
case *rsa.PublicKey:
pem.Encode(w, &pem.Block{
Type: "RSA PUBLIC KEY",
Bytes: x509.MarshalPKCS1PublicKey(pub),
})
case *ecdsa.PublicKey:
// skip error since we're type safe already
bytes, _ := x509.MarshalPKIXPublicKey(pub)
pem.Encode(w, &pem.Block{
Type: "PUBLIC KEY",
Bytes: bytes,
})
default:
w.Write([]byte("Sanity Error: Impossible key type"))
}
}
func marshalJWKs(keys []keypairs.PublicKeyDeprecated, exp2 time.Time) []string {
jwks := make([]string, 0, 1)
for i := range keys {
key := keys[i]
// if the key itself has an expiry, let that override
exp := key.ExpiresAt()
if never == exp {
// otherwise use our default
exp = exp2
}
// Note that you don't have to embed `iss` in the JWK because the client
// already has that info by virtue of getting to it in the first place.
pub := key.Key()
jwk := string(keypairs.MarshalJWKPublicKey(pub, exp))
jwks = append(jwks, jwk)
}
return jwks
}

102
keyserve/keyserve_test.go Normal file
View File

@ -0,0 +1,102 @@
package keyserve
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"encoding/json"
"io/ioutil"
"log"
"net/http"
"os"
"testing"
"time"
"git.rootprojects.org/root/keypairs"
)
func TestServeKeys(t *testing.T) {
eckey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
pubs := []keypairs.PublicKeyDeprecated{
keypairs.NewPublicKey(eckey.Public()),
}
addr := ":62017"
done := make(chan bool, 1)
h := &http.Server{Addr: addr, Handler: &server{Middleware: Middleware{Keys: pubs}}}
logger := log.New(os.Stdout, "", 0)
go func() {
logger.Printf("Listening on http://0.0.0.0%s\n", addr)
if err := h.ListenAndServe(); err != nil {
// TODO check for the non-error server closed error
//logger.Fatal(err)
}
done <- true
}()
go func() {
time.Sleep(15 * time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
h.Shutdown(ctx)
cancel()
}()
m := map[string]string{}
resp, err := http.Get("http://localhost" + addr + "/.well-known/openid-configuration")
if nil != err {
log.Fatal(err)
}
dec := json.NewDecoder(resp.Body)
err = dec.Decode(&m)
if nil != err {
log.Fatal(err)
}
n := struct {
Keys []map[string]interface{} `json:"keys"`
}{
Keys: []map[string]interface{}{},
}
resp, err = http.Get(m["jwks_uri"])
if nil != err {
log.Fatal(err)
}
dec = json.NewDecoder(resp.Body)
err = dec.Decode(&n)
if nil != err {
log.Fatal(err)
}
resp, err = http.Get("http://localhost" + addr + "/pem")
if nil != err {
log.Fatal(err)
}
bytes, err := ioutil.ReadAll(resp.Body)
if nil != err {
log.Fatal(err)
}
_, err = keypairs.ParsePublicKey(bytes)
if nil != err {
log.Fatal("Could not parse PEM/cert from self")
log.Fatal(err)
}
h.Shutdown(context.Background())
<-done
}
type server struct {
Middleware Middleware
}
func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if !s.Middleware.Handler(w, r) {
w.Write([]byte("Try .well-known/openid-configuration or .well-known/jwks.json"))
}
}

170
marshal.go Normal file
View File

@ -0,0 +1,170 @@
package keypairs
import (
"crypto/ecdsa"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"fmt"
"log"
"math/big"
mathrand "math/rand"
)
// MarshalPEMPublicKey outputs the given public key as JWK
func MarshalPEMPublicKey(pubkey PublicKey) ([]byte, error) {
block, err := marshalDERPublicKey(pubkey)
if nil != err {
return nil, err
}
return pem.EncodeToMemory(block), nil
}
// MarshalDERPublicKey outputs the given public key as JWK
func MarshalDERPublicKey(pubkey PublicKey) ([]byte, error) {
block, err := marshalDERPublicKey(pubkey)
if nil != err {
return nil, err
}
return block.Bytes, nil
}
// marshalDERPublicKey outputs the given public key as JWK
func marshalDERPublicKey(pubkey PublicKey) (*pem.Block, error) {
var der []byte
var typ string
var err error
switch k := pubkey.(type) {
case *rsa.PublicKey:
der = x509.MarshalPKCS1PublicKey(k)
typ = "RSA PUBLIC KEY"
case *ecdsa.PublicKey:
typ = "PUBLIC KEY"
der, err = x509.MarshalPKIXPublicKey(k)
if nil != err {
return nil, err
}
default:
panic("Developer Error: impossible key type")
}
return &pem.Block{
Bytes: der,
Type: typ,
}, nil
}
// MarshalJWKPrivateKey outputs the given private key as JWK
func MarshalJWKPrivateKey(privkey PrivateKey) []byte {
// thumbprint keys are alphabetically sorted and only include the necessary public parts
switch k := privkey.(type) {
case *rsa.PrivateKey:
return MarshalRSAPrivateKey(k)
case *ecdsa.PrivateKey:
return MarshalECPrivateKey(k)
default:
// this is unreachable because we know the types that we pass in
log.Printf("keytype: %t, %+v\n", privkey, privkey)
panic(ErrInvalidPublicKey)
//return nil
}
}
// MarshalDERPrivateKey outputs the given private key as ASN.1 DER
func MarshalDERPrivateKey(privkey PrivateKey) ([]byte, error) {
// thumbprint keys are alphabetically sorted and only include the necessary public parts
switch k := privkey.(type) {
case *rsa.PrivateKey:
return x509.MarshalPKCS1PrivateKey(k), nil
case *ecdsa.PrivateKey:
return x509.MarshalECPrivateKey(k)
default:
// this is unreachable because we know the types that we pass in
log.Printf("keytype: %t, %+v\n", privkey, privkey)
panic(ErrInvalidPublicKey)
//return nil, nil
}
}
func marshalDERPrivateKey(privkey PrivateKey) (*pem.Block, error) {
var typ string
var bytes []byte
var err error
switch k := privkey.(type) {
case *rsa.PrivateKey:
if 0 == mathrand.Intn(2) {
typ = "PRIVATE KEY"
bytes, err = x509.MarshalPKCS8PrivateKey(k)
if nil != err {
return nil, err
}
} else {
typ = "RSA PRIVATE KEY"
bytes = x509.MarshalPKCS1PrivateKey(k)
}
return &pem.Block{
Type: typ,
Bytes: bytes,
}, nil
case *ecdsa.PrivateKey:
if 0 == mathrand.Intn(2) {
typ = "PRIVATE KEY"
bytes, err = x509.MarshalPKCS8PrivateKey(k)
} else {
typ = "EC PRIVATE KEY"
bytes, err = x509.MarshalECPrivateKey(k)
}
if nil != err {
return nil, err
}
return &pem.Block{
Type: typ,
Bytes: bytes,
}, nil
default:
// this is unreachable because we know the types that we pass in
log.Printf("keytype: %t, %+v\n", privkey, privkey)
panic(ErrInvalidPublicKey)
//return nil, nil
}
}
// MarshalPEMPrivateKey outputs the given private key as ASN.1 PEM
func MarshalPEMPrivateKey(privkey PrivateKey) ([]byte, error) {
block, err := marshalDERPrivateKey(privkey)
if nil != err {
return nil, err
}
return pem.EncodeToMemory(block), nil
}
// MarshalECPrivateKey will output the given private key as JWK
func MarshalECPrivateKey(k *ecdsa.PrivateKey) []byte {
crv := k.Curve.Params().Name
d := base64.RawURLEncoding.EncodeToString(k.D.Bytes())
x := base64.RawURLEncoding.EncodeToString(k.X.Bytes())
y := base64.RawURLEncoding.EncodeToString(k.Y.Bytes())
return []byte(fmt.Sprintf(
`{"crv":%q,"d":%q,"kty":"EC","x":%q,"y":%q}`,
crv, d, x, y,
))
}
// MarshalRSAPrivateKey will output the given private key as JWK
func MarshalRSAPrivateKey(pk *rsa.PrivateKey) []byte {
e := base64.RawURLEncoding.EncodeToString(big.NewInt(int64(pk.E)).Bytes())
n := base64.RawURLEncoding.EncodeToString(pk.N.Bytes())
d := base64.RawURLEncoding.EncodeToString(pk.D.Bytes())
p := base64.RawURLEncoding.EncodeToString(pk.Primes[0].Bytes())
q := base64.RawURLEncoding.EncodeToString(pk.Primes[1].Bytes())
dp := base64.RawURLEncoding.EncodeToString(pk.Precomputed.Dp.Bytes())
dq := base64.RawURLEncoding.EncodeToString(pk.Precomputed.Dq.Bytes())
qi := base64.RawURLEncoding.EncodeToString(pk.Precomputed.Qinv.Bytes())
return []byte(fmt.Sprintf(
`{"d":%q,"dp":%q,"dq":%q,"e":%q,"kty":"RSA","n":%q,"p":%q,"q":%q,"qi":%q}`,
d, dp, dq, e, n, p, q, qi,
))
}

46
mock.go Normal file
View 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
}

165
sign.go Normal file
View File

@ -0,0 +1,165 @@
package keypairs
import (
"crypto"
"crypto/ecdsa"
"crypto/rsa"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
mathrand "math/rand" // to be used for good, not evil
"time"
)
// Object is a type alias representing generic JSON data
type Object = map[string]interface{}
// SignClaims adds `typ`, `kid` (or `jwk`), and `alg` in the header and expects claims for `jti`, `exp`, `iss`, and `iat`
func SignClaims(privkey PrivateKey, header Object, claims Object) (*JWS, error) {
var randsrc io.Reader = randReader
seed, _ := header["_seed"].(int64)
if 0 != seed {
randsrc = mathrand.New(mathrand.NewSource(seed))
//delete(header, "_seed")
}
protected, header, err := headerToProtected(privkey.Public().(PublicKey), 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)
signable := fmt.Sprintf(`%s.%s`, protected64, payload64)
hash := sha256.Sum256([]byte(signable))
sig := Sign(privkey, hash[:], randsrc)
sig64 := base64.RawURLEncoding.EncodeToString(sig)
//log.Printf("\n(Sign)\nSignable: %s", signable)
//log.Printf("Hash: %s", hash)
//log.Printf("Sig: %s", sig64)
return &JWS{
Header: header,
Claims: claims,
Protected: protected64,
Payload: payload64,
Signature: sig64,
}, nil
}
func headerToProtected(pub PublicKey, header Object) ([]byte, Object, 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.(type) {
case *rsa.PublicKey:
alg = "RS256"
}
if selfSign, _ := header["_jwk"].(bool); selfSign {
delete(header, "_jwk")
any := Object{}
_ = json.Unmarshal(MarshalJWKPublicKey(pub), &any)
header["jwk"] = any
}
// TODO what are the acceptable values? JWT. JWS? others?
header["typ"] = "JWT"
if _, ok := header["jwk"]; !ok {
thumbprint := ThumbprintPublicKey(NewPublicKey(pub))
kid, _ := header["kid"].(string)
if "" != kid && thumbprint != kid {
return nil, 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, nil, err
}
return protected, header, nil
}
func claimsToPayload(claims Object) ([]byte, error) {
if nil == claims {
claims = Object{}
}
var dur time.Duration
jti, _ := claims["jti"].(string)
insecure, _ := claims["insecure"].(bool)
switch exp := claims["exp"].(type) {
case time.Duration:
// TODO: MUST this go first?
// int64(time.Duration) vs time.Duration(int64)
dur = exp
case string:
var err error
dur, err = time.ParseDuration(exp)
// TODO s, err := time.ParseDuration(dur)
if nil != err {
return nil, err
}
case int:
dur = time.Second * time.Duration(exp)
case int64:
dur = time.Second * time.Duration(exp)
case float64:
dur = time.Second * time.Duration(exp)
default:
dur = 0
}
if "" == jti && 0 == dur && !insecure {
return nil, errors.New("token must have jti or exp as to be expirable / cancellable")
}
claims["exp"] = time.Now().Add(dur).Unix()
return json.Marshal(claims)
}
// 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 {
if nil == rand {
rand = randReader
}
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()
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
}

174
verify.go Normal file
View 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 PublicKeyDeprecated = 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.Key(), 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 = privkey.Public().(PublicKey)
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(Thumbprint(pub))) {
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.(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
}
}