mirror of
https://github.com/therootcompany/keypairs
synced 2025-04-21 15:00:44 +00:00
Compare commits
No commits in common. "master" and "v0.2.0" have entirely different histories.
5
.gitignore
vendored
5
.gitignore
vendored
@ -1,5 +0,0 @@
|
|||||||
/keypairs
|
|
||||||
/dist/
|
|
||||||
|
|
||||||
.DS_Store
|
|
||||||
.*.sw*
|
|
@ -1,41 +0,0 @@
|
|||||||
# 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:'
|
|
21
LICENSE
21
LICENSE
@ -1,21 +0,0 @@
|
|||||||
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
106
README.md
@ -1,102 +1,56 @@
|
|||||||
# [keypairs](https://git.rootprojects.org/root/keypairs)
|
# go-keypairs
|
||||||
|
|
||||||
A cross-platform Command Line Tool and Golang Library that works
|
The lightest touch over top of Go's `crypto/ecdsa` and `crypto/rsa` to make them
|
||||||
with RSA, ECDSA, PEM, DER, JWK, and the JOSE suite.
|
*typesafe* and to provide JSON Web Key (JWK) support.
|
||||||
|
|
||||||
# Keypairs CLI
|
# Documentation
|
||||||
|
|
||||||
Generates, signs, and verifies with NIST-strength asymmetric keys.
|
Use the source, Luke!
|
||||||
|
|
||||||
```bash
|
<https://godoc.org/github.com/big-squid/go-keypairs>
|
||||||
# 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
|
||||||
|
|
||||||
Go's standard library is great.
|
Always remember:
|
||||||
|
|
||||||
Go has _excellent_ crytography support and provides wonderful
|
> Don't roll your own crypto.
|
||||||
primitives for dealing with them.
|
|
||||||
|
|
||||||
I prefer to stay as close to Go's `crypto` package as possible,
|
But also remember:
|
||||||
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
|
||||||
|
|
||||||
`crypto.PublicKey` is a "marker interface", meaning that it is **not typesafe**!
|
Go has _excellent_ crytography support and provides wonderful
|
||||||
|
primitives for dealing with them. Its Achilles' heel is they're **not typesafe**!
|
||||||
|
|
||||||
`go-keypairs` defines `type keypairs.PrivateKey interface { Public() crypto.PublicKey }`,
|
As of Go 1.11.5 `crypto.PublicKey` and `crypto.PrivateKey` are "marker interfaces"
|
||||||
|
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 (JWK) as a "codec"
|
# JSON Web Key "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
19
cli_test.sh
@ -1,19 +0,0 @@
|
|||||||
#!/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
|
|
@ -1,477 +0,0 @@
|
|||||||
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
2
doc.go
@ -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 "git.rootprojects.org/root/keypairs/keyfetch"
|
import "github.com/big-squid/go-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/")
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
module github.com/example/foobar
|
|
||||||
|
|
||||||
go 1.12
|
|
||||||
|
|
||||||
replace github.com/big-squid/go-keypairs => git.rootprojects.org/root/keypairs
|
|
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"kty": "EC",
|
|
||||||
"crv": "P-256",
|
|
||||||
"x": "IT1SWLxsacPiE5Z16jkopAn8_-85rMjgyCokrnjDft4",
|
|
||||||
"y": "mP2JwOAOdMmXuwpxbKng3KZz27mz-nKWIlXJ3rzSGMo"
|
|
||||||
}
|
|
69
generate.go
69
generate.go
@ -1,69 +0,0 @@
|
|||||||
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
4
go.mod
@ -1,3 +1 @@
|
|||||||
module git.rootprojects.org/root/keypairs
|
module github.com/big-squid/go-keypairs
|
||||||
|
|
||||||
go 1.12
|
|
||||||
|
69
jwk.go
69
jwk.go
@ -1,69 +0,0 @@
|
|||||||
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
63
jws.go
@ -1,63 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -11,7 +11,6 @@ package keyfetch
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -19,29 +18,16 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.rootprojects.org/root/keypairs"
|
keypairs "github.com/big-squid/go-keypairs"
|
||||||
"git.rootprojects.org/root/keypairs/keyfetch/uncached"
|
"github.com/big-squid/go-keypairs/keyfetch/uncached"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO should be ErrInvalidJWKURL
|
var EInvalidJWKURL = errors.New("url does not lead to valid JWKs")
|
||||||
|
|
||||||
// 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.PublicKeyDeprecated
|
Key keypairs.PublicKey
|
||||||
Expiry time.Time
|
Expiry time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,64 +53,50 @@ 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
|
||||||
|
|
||||||
// PublicKeysMap is a newtype for a map of keypairs.PublicKey
|
type publicKeysMap map[string]keypairs.PublicKey
|
||||||
type PublicKeysMap = map[string]keypairs.PublicKeyDeprecated
|
|
||||||
|
|
||||||
// OIDCJWKs fetches baseURL + ".well-known/openid-configuration" and then fetches and returns the Public Keys.
|
// FetchOIDCPublicKeys fetches baseURL + ".well-known/openid-configuration" and then returns FetchPublicKeys(jwks_uri).
|
||||||
func OIDCJWKs(baseURL string) (PublicKeysMap, error) {
|
func OIDCJWKs(baseURL string) (publicKeysMap, error) {
|
||||||
maps, keys, err := uncached.OIDCJWKs(baseURL)
|
if maps, keys, err := uncached.OIDCJWKs(baseURL); nil != err {
|
||||||
|
|
||||||
if nil != err {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
|
} else {
|
||||||
|
cacheKeys(maps, keys, baseURL)
|
||||||
|
return keys, err
|
||||||
}
|
}
|
||||||
cacheKeys(maps, keys, baseURL)
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
// WellKnownJWKs fetches baseURL + ".well-known/jwks.json" and caches and returns the keys
|
func WellKnownJWKs(kidOrThumb, iss string) (publicKeysMap, error) {
|
||||||
func WellKnownJWKs(kidOrThumb, iss string) (PublicKeysMap, error) {
|
if maps, keys, err := uncached.WellKnownJWKs(iss); nil != err {
|
||||||
maps, keys, err := uncached.WellKnownJWKs(iss)
|
|
||||||
|
|
||||||
if nil != err {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
|
} else {
|
||||||
|
cacheKeys(maps, keys, iss)
|
||||||
|
return keys, err
|
||||||
}
|
}
|
||||||
cacheKeys(maps, keys, iss)
|
|
||||||
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) {
|
||||||
maps, keys, err := uncached.JWKs(jwksurl)
|
if maps, keys, err := uncached.JWKs(jwksurl); nil != err {
|
||||||
|
|
||||||
if nil != err {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
|
} else {
|
||||||
|
iss := strings.Replace(jwksurl, ".well-known/jwks.json", "", 1)
|
||||||
|
cacheKeys(maps, keys, iss)
|
||||||
|
return keys, err
|
||||||
}
|
}
|
||||||
iss := strings.Replace(jwksurl, ".well-known/jwks.json", "", 1)
|
|
||||||
cacheKeys(maps, keys, iss)
|
|
||||||
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
|
||||||
@ -135,31 +107,20 @@ func JWK(kidOrThumb, iss string) (keypairs.PublicKey, error) {
|
|||||||
// PEM tries to return a key from cache, falling back to the specified PEM url
|
// PEM tries to return a key from cache, falling back to the specified PEM url
|
||||||
func PEM(url string) (keypairs.PublicKey, error) {
|
func PEM(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.PublicKeyDeprecated, error) {
|
return immediateOneOrFetch(url, url, func(string) (map[string]map[string]string, map[string]keypairs.PublicKey, error) {
|
||||||
m, key, err := uncached.PEM(url)
|
m, key, err := uncached.PEM(url)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
return nil, 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
|
// put in a map, just for caching
|
||||||
maps := map[string]map[string]string{}
|
maps := map[string]map[string]string{}
|
||||||
maps[keypairs.Thumbprint(key)] = m
|
maps[key.Thumbprint()] = m
|
||||||
maps[url] = m
|
maps[url] = m
|
||||||
|
|
||||||
keys := uncached.PublicKeysMap{} // map[string]keypairs.PublicKeyDeprecated{}
|
keys := map[string]keypairs.PublicKey{}
|
||||||
keys[keypairs.Thumbprint(key)] = pubd
|
keys[key.Thumbprint()] = key
|
||||||
keys[url] = pubd
|
keys[url] = key
|
||||||
|
|
||||||
return maps, keys, nil
|
return maps, keys, nil
|
||||||
})
|
})
|
||||||
@ -168,29 +129,28 @@ func PEM(url string) (keypairs.PublicKey, error) {
|
|||||||
// 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,
|
return immediateOneOrFetch(url, url, func(string) (map[string]map[string]string, map[string]keypairs.PublicKey, error) {
|
||||||
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
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 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[keypairs.Thumbprint(key.Key())] = m
|
maps[key.Thumbprint()] = m
|
||||||
|
|
||||||
keys := map[string]keypairs.PublicKeyDeprecated{}
|
keys := map[string]keypairs.PublicKey{}
|
||||||
keys[keypairs.Thumbprint(key.Key())] = key
|
keys[key.Thumbprint()] = key
|
||||||
|
|
||||||
return maps, keys, nil
|
return maps, keys, nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get retrieves a key from cache, or returns an error.
|
// Get retrieves a key from cache, or returns an 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.Key()
|
return pub.Key
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -224,21 +184,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()
|
||||||
hit := get(kidOrThumb, iss)
|
key := get(kidOrThumb, iss)
|
||||||
|
|
||||||
if nil == hit {
|
if nil == key {
|
||||||
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 hit.Expiry.Sub(now) <= StaleTime {
|
if key.Expiry.Sub(now) <= StaleTime {
|
||||||
go fetchAndSelect(kidOrThumb, iss, fetcher)
|
go fetchAndSelect(kidOrThumb, iss, fetcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
return hit.Key.Key(), nil
|
return key.Key, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type myfetcher func(string) (map[string]map[string]string, map[string]keypairs.PublicKeyDeprecated, error)
|
type myfetcher func(string) (map[string]map[string]string, map[string]keypairs.PublicKey, 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)
|
||||||
@ -249,21 +209,20 @@ 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 == keypairs.Thumbprint(pub) {
|
if id == key.Thumbprint() {
|
||||||
return pub, nil
|
return key, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if id == key.KeyID() {
|
if id == key.KeyID() {
|
||||||
return pub, nil
|
return key, 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 PublicKeysMap, issuer string) {
|
func cacheKeys(maps map[string]map[string]string, keys map[string]keypairs.PublicKey, issuer string) {
|
||||||
for i := range keys {
|
for i := range keys {
|
||||||
key := keys[i]
|
key := keys[i]
|
||||||
m := maps[i]
|
m := maps[i]
|
||||||
@ -273,13 +232,10 @@ func cacheKeys(maps map[string]map[string]string, keys PublicKeysMap, issuer str
|
|||||||
}
|
}
|
||||||
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.PublicKeyDeprecated) error {
|
func cacheKey(kid, iss, expstr string, pub keypairs.PublicKey) error {
|
||||||
var expiry time.Time
|
var expiry time.Time
|
||||||
iss = normalizeIssuer(iss)
|
iss = normalizeIssuer(iss)
|
||||||
|
|
||||||
@ -303,7 +259,7 @@ func cacheKey(kid, iss, expstr string, pub keypairs.PublicKeyDeprecated) error {
|
|||||||
Expiry: expiry,
|
Expiry: expiry,
|
||||||
}
|
}
|
||||||
// Since thumbprints are crypto secure, iss isn't needed
|
// Since thumbprints are crypto secure, iss isn't needed
|
||||||
thumb := keypairs.Thumbprint(pub.Key())
|
thumb := pub.Thumbprint()
|
||||||
KeyCache[thumb] = CachableKey{
|
KeyCache[thumb] = CachableKey{
|
||||||
Key: pub,
|
Key: pub,
|
||||||
Expiry: expiry,
|
Expiry: expiry,
|
||||||
@ -322,11 +278,36 @@ 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 {
|
IsTrustedIssuer returns true when the `iss` (i.e. from a token) matches one
|
||||||
return false
|
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 IsTrustedIssuer(iss string, whitelist Whitelist, rs ...*http.Request) bool {
|
||||||
// Normalize the http:// and https:// and parse
|
// Normalize the http:// and https:// and parse
|
||||||
iss = strings.TrimRight(iss, "/") + "/"
|
iss = strings.TrimRight(iss, "/") + "/"
|
||||||
if strings.HasPrefix(iss, "http://") {
|
if strings.HasPrefix(iss, "http://") {
|
||||||
@ -421,47 +402,23 @@ func hasImplicitTrust(issURL *url.URL, r *http.Request) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Whitelist is a newtype for an array of URLs
|
// Whitelist
|
||||||
type Whitelist []*url.URL
|
type Whitelist []*url.URL
|
||||||
|
|
||||||
// NewWhitelist turns an array of URLs (such as https://example.com/) into
|
// 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
|
// a parsed array of *url.URLs that can be used by the IsTrustedIssuer function
|
||||||
func NewWhitelist(issuers []string, privateList ...[]string) (Whitelist, error) {
|
func NewWhitelist(issuers []string, insecures ...bool) (Whitelist, error) {
|
||||||
var err error
|
|
||||||
|
|
||||||
list := []*url.URL{}
|
list := []*url.URL{}
|
||||||
if 0 != len(issuers) {
|
insecure := false
|
||||||
insecure := false
|
if 0 != len(insecures) && insecures[0] {
|
||||||
list, err = newWhitelist(list, issuers, insecure)
|
insecure = true
|
||||||
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 {
|
for i := range issuers {
|
||||||
iss := issuers[i]
|
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 strings.HasPrefix(iss, "http://") {
|
||||||
if !insecure {
|
if !insecure {
|
||||||
log.Println("Oops! You have an insecure domain in your whitelist: ", iss)
|
return nil, errors.New("Oops! You have an insecure domain in your whitelist: " + iss)
|
||||||
return nil, ErrInsecureDomain
|
|
||||||
}
|
}
|
||||||
} else if strings.HasPrefix(iss, "//") {
|
} else if strings.HasPrefix(iss, "//") {
|
||||||
// TODO
|
// TODO
|
||||||
@ -469,64 +426,17 @@ func newWhitelist(list []*url.URL, issuers []string, insecure bool) (Whitelist,
|
|||||||
} else if !strings.HasPrefix(iss, "https://") {
|
} else if !strings.HasPrefix(iss, "https://") {
|
||||||
iss = "https://" + iss
|
iss = "https://" + iss
|
||||||
}
|
}
|
||||||
|
|
||||||
// trailing slash as a boundary character, which may or may not denote a directory
|
// trailing slash as a boundary character, which may or may not denote a directory
|
||||||
iss = strings.TrimRight(iss, "/") + "/"
|
iss = strings.TrimRight(iss, "/") + "/"
|
||||||
u, err := url.Parse(iss)
|
u, err := url.Parse(iss)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Strip any * prefix, for easier comparison later
|
|
||||||
// *.example.com => .example.com
|
|
||||||
if strings.HasPrefix(u.Host, "*.") {
|
if strings.HasPrefix(u.Host, "*.") {
|
||||||
u.Host = u.Host[1:]
|
u.Host = u.Host[1:]
|
||||||
}
|
}
|
||||||
|
|
||||||
list = append(list, u)
|
list = append(list, u)
|
||||||
}
|
}
|
||||||
|
|
||||||
return list, nil
|
return Whitelist(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, " ")
|
|
||||||
}
|
}
|
||||||
|
@ -4,40 +4,28 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.rootprojects.org/root/keypairs"
|
keypairs "github.com/big-squid/go-keypairs"
|
||||||
"git.rootprojects.org/root/keypairs/keyfetch/uncached"
|
"github.com/big-squid/go-keypairs/keyfetch/uncached"
|
||||||
)
|
)
|
||||||
|
|
||||||
var pubkey keypairs.PublicKey
|
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
|
// Get PEM
|
||||||
pubk3, err := PEM("https://bigsquid.auth0.com/pem")
|
k3, err := PEM("https://bigsquid.auth0.com/pem")
|
||||||
if nil != err {
|
if nil != err {
|
||||||
t.Fatal("[0] Error fetching and caching key:", err)
|
t.Fatal("Error fetching and caching key:", err)
|
||||||
}
|
}
|
||||||
thumb3 := keypairs.Thumbprint(pubk3)
|
if k3.Thumbprint() != pubkey.Thumbprint() {
|
||||||
thumb := keypairs.Thumbprint(pubkey)
|
t.Fatal("Error got different thumbprint for different versions of the same key:", err)
|
||||||
if thumb3 != thumb {
|
|
||||||
t.Fatalf("Error got different thumbprint for different versions of the same key %q != %q: %v", thumb3, thumb, err)
|
|
||||||
}
|
}
|
||||||
clear()
|
clear()
|
||||||
testCachesKey(t, "https://big-squid.github.io/")
|
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) {
|
||||||
// Raw fetch a key and get KID and Thumbprint
|
// Raw fetch a key and get KID and Thumbprint
|
||||||
_, keys, err := uncached.OIDCJWKs(url)
|
_, keys, err := uncached.OIDCJWKs(url)
|
||||||
@ -50,10 +38,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()
|
key = keys[i]
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
thumb := keypairs.Thumbprint(key)
|
thumb := key.Thumbprint()
|
||||||
|
|
||||||
// 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 {
|
||||||
@ -63,25 +51,20 @@ func testCachesKey(t *testing.T, url string) {
|
|||||||
// Get with caching
|
// Get with caching
|
||||||
pubkey, err = OIDCJWK(thumb, url)
|
pubkey, err = OIDCJWK(thumb, url)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
t.Fatal("[1] Error fetching and caching key:", err)
|
t.Fatal("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 "" != pubkey.KeyID() {
|
||||||
// TODO thumb / id mapping
|
if pub := Get(pubkey.KeyID(), url); nil == pub {
|
||||||
thumb = keypairs.Thumbprint(pubkey)
|
t.Fatal("key was not properly cached by kid", pubkey.KeyID())
|
||||||
if pub := Get(thumb, url); nil == pub {
|
}
|
||||||
t.Fatal("key was not properly cached by kid", pubkey)
|
} else {
|
||||||
|
t.Log("Key did not have an explicit KeyID")
|
||||||
}
|
}
|
||||||
// TODO
|
|
||||||
/*
|
|
||||||
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()
|
||||||
@ -94,10 +77,8 @@ 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.Equal(pubkey) || keypairs.Thumbprint(key) != keypairs.Thumbprint(pubkey) {
|
if key.KeyID() != pubkey.KeyID() || key.Thumbprint() != pubkey.Thumbprint() {
|
||||||
t.Fatalf("SANITY: [todo: KeyIDs or] Thumbprints do not match:\n%q != %q\n%q != %q",
|
t.Fatal("SANITY: KeyIDs or Thumbprints do not match:", key.KeyID(), pubkey.KeyID(), key.Thumbprint(), pubkey.Thumbprint())
|
||||||
keypairs.Thumbprint(key), keypairs.Thumbprint(pubkey),
|
|
||||||
keypairs.Thumbprint(key), keypairs.Thumbprint(pubkey))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get 404
|
// Get 404
|
||||||
|
@ -20,127 +20,117 @@ func TestInvalidIssuer(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestIssuerMatches(t *testing.T) {
|
func TestIssuerMatches(t *testing.T) {
|
||||||
// because [""] = strings.Split(os.Getenv("DOESNTEXIST"), ",")
|
|
||||||
trusted := []string{
|
trusted := []string{
|
||||||
"",
|
|
||||||
"https://example.com/",
|
"https://example.com/",
|
||||||
|
"http://happy.xyz/abc",
|
||||||
"foobar.net/def/",
|
"foobar.net/def/",
|
||||||
"https://*.wild.org",
|
"https://*.wild.org",
|
||||||
"https://*.west.mali/verde",
|
"https://*.west.mali/verde",
|
||||||
}
|
}
|
||||||
privates := []string{
|
|
||||||
"http://happy.xyz/abc",
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := NewWhitelist(append(trusted, privates...))
|
_, err := NewWhitelist(trusted)
|
||||||
if nil == err {
|
if nil == err {
|
||||||
t.Fatal(errors.New("an insecure domain got through"))
|
t.Fatal(errors.New("An insecure domain got through!"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Empty list is allowed... I guess?
|
list, err := NewWhitelist(trusted, true)
|
||||||
list, err := NewWhitelist(nil)
|
|
||||||
if nil != err {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
// Combo list
|
|
||||||
list, err = NewWhitelist(trusted[1:], privates)
|
|
||||||
if nil != err {
|
if nil != err {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var iss string
|
var iss string
|
||||||
iss = "https://example.com"
|
iss = "https://example.com"
|
||||||
if !list.IsTrustedIssuer(iss) {
|
if !IsTrustedIssuer(iss, list) {
|
||||||
t.Fatal("A good domain didn't make it:", iss)
|
t.Fatal("A good domain didn't make it:", iss)
|
||||||
}
|
}
|
||||||
|
|
||||||
iss = "https://example.com/"
|
iss = "https://example.com/"
|
||||||
if !list.IsTrustedIssuer(iss) {
|
if !IsTrustedIssuer(iss, list) {
|
||||||
t.Fatal("A good domain didn't make it:", iss)
|
t.Fatal("A good domain didn't make it:", iss)
|
||||||
}
|
}
|
||||||
|
|
||||||
iss = "http://example.com"
|
iss = "http://example.com"
|
||||||
if list.IsTrustedIssuer(iss) {
|
if IsTrustedIssuer(iss, list) {
|
||||||
t.Fatal("A bad URL slipped past", iss)
|
t.Fatal("A bad URL slipped past", iss)
|
||||||
}
|
}
|
||||||
|
|
||||||
iss = "https://example.com/foo"
|
iss = "https://example.com/foo"
|
||||||
if list.IsTrustedIssuer(iss) {
|
if IsTrustedIssuer(iss, list) {
|
||||||
t.Fatal("A bad URL slipped past", iss)
|
t.Fatal("A bad URL slipped past", iss)
|
||||||
}
|
}
|
||||||
|
|
||||||
iss = "http://happy.xyz/abc"
|
iss = "http://happy.xyz/abc"
|
||||||
if !list.IsTrustedIssuer(iss) {
|
if !IsTrustedIssuer(iss, list) {
|
||||||
t.Fatal("A good URL didn't make it:", iss)
|
t.Fatal("A good URL didn't make it:", iss)
|
||||||
}
|
}
|
||||||
|
|
||||||
iss = "http://happy.xyz/abc/"
|
iss = "http://happy.xyz/abc/"
|
||||||
if !list.IsTrustedIssuer(iss) {
|
if !IsTrustedIssuer(iss, list) {
|
||||||
t.Fatal("A good URL didn't make it:", iss)
|
t.Fatal("A good URL didn't make it:", iss)
|
||||||
}
|
}
|
||||||
|
|
||||||
iss = "http://happy.xyz/abc/d"
|
iss = "http://happy.xyz/abc/d"
|
||||||
if list.IsTrustedIssuer(iss) {
|
if IsTrustedIssuer(iss, list) {
|
||||||
t.Fatal("A bad URL slipped past", iss)
|
t.Fatal("A bad URL slipped past", iss)
|
||||||
}
|
}
|
||||||
|
|
||||||
iss = "http://happy.xyz/abcd"
|
iss = "http://happy.xyz/abcd"
|
||||||
if list.IsTrustedIssuer(iss) {
|
if IsTrustedIssuer(iss, list) {
|
||||||
t.Fatal("A bad URL slipped past", iss)
|
t.Fatal("A bad URL slipped past", iss)
|
||||||
}
|
}
|
||||||
|
|
||||||
iss = "https://foobar.net/def"
|
iss = "https://foobar.net/def"
|
||||||
if !list.IsTrustedIssuer(iss) {
|
if !IsTrustedIssuer(iss, list) {
|
||||||
t.Fatal("A good URL didn't make it:", iss)
|
t.Fatal("A good URL didn't make it:", iss)
|
||||||
}
|
}
|
||||||
|
|
||||||
iss = "https://foobar.net/def/"
|
iss = "https://foobar.net/def/"
|
||||||
if !list.IsTrustedIssuer(iss) {
|
if !IsTrustedIssuer(iss, list) {
|
||||||
t.Fatal("A good URL didn't make it:", iss)
|
t.Fatal("A good URL didn't make it:", iss)
|
||||||
}
|
}
|
||||||
|
|
||||||
iss = "http://foobar.net/def/"
|
iss = "http://foobar.net/def/"
|
||||||
if list.IsTrustedIssuer(iss) {
|
if IsTrustedIssuer(iss, list) {
|
||||||
t.Fatal("A bad URL slipped past", iss)
|
t.Fatal("A bad URL slipped past", iss)
|
||||||
}
|
}
|
||||||
|
|
||||||
iss = "https://foobar.net/def/e"
|
iss = "https://foobar.net/def/e"
|
||||||
if list.IsTrustedIssuer(iss) {
|
if IsTrustedIssuer(iss, list) {
|
||||||
t.Fatal("A bad URL slipped past", iss)
|
t.Fatal("A bad URL slipped past", iss)
|
||||||
}
|
}
|
||||||
|
|
||||||
iss = "https://foobar.net/defe"
|
iss = "https://foobar.net/defe"
|
||||||
if list.IsTrustedIssuer(iss) {
|
if IsTrustedIssuer(iss, list) {
|
||||||
t.Fatal("A bad URL slipped past", iss)
|
t.Fatal("A bad URL slipped past", iss)
|
||||||
}
|
}
|
||||||
|
|
||||||
iss = "https://wild.org"
|
iss = "https://wild.org"
|
||||||
if list.IsTrustedIssuer(iss) {
|
if IsTrustedIssuer(iss, list) {
|
||||||
t.Fatal("A bad URL slipped past", iss)
|
t.Fatal("A bad URL slipped past", iss)
|
||||||
}
|
}
|
||||||
|
|
||||||
iss = "https://foo.wild.org"
|
iss = "https://foo.wild.org"
|
||||||
if !list.IsTrustedIssuer(iss) {
|
if !IsTrustedIssuer(iss, list) {
|
||||||
t.Fatal("A good URL didn't make it:", iss)
|
t.Fatal("A good URL didn't make it:", iss)
|
||||||
}
|
}
|
||||||
|
|
||||||
iss = "https://sub.foo.wild.org"
|
iss = "https://sub.foo.wild.org"
|
||||||
if !list.IsTrustedIssuer(iss) {
|
if !IsTrustedIssuer(iss, list) {
|
||||||
t.Fatal("A good URL didn't make it:", iss)
|
t.Fatal("A good URL didn't make it:", iss)
|
||||||
}
|
}
|
||||||
|
|
||||||
iss = "https://foo.wild.org/cherries"
|
iss = "https://foo.wild.org/cherries"
|
||||||
if list.IsTrustedIssuer(iss) {
|
if IsTrustedIssuer(iss, list) {
|
||||||
t.Fatal("A bad URL slipped past", iss)
|
t.Fatal("A bad URL slipped past", iss)
|
||||||
}
|
}
|
||||||
|
|
||||||
iss = "https://sub.west.mali/verde/"
|
iss = "https://sub.west.mali/verde/"
|
||||||
if !list.IsTrustedIssuer(iss) {
|
if !IsTrustedIssuer(iss, list) {
|
||||||
t.Fatal("A good URL didn't make it:", iss)
|
t.Fatal("A good URL didn't make it:", iss)
|
||||||
}
|
}
|
||||||
|
|
||||||
iss = "https://sub.west.mali"
|
iss = "https://sub.west.mali"
|
||||||
if list.IsTrustedIssuer(iss) {
|
if IsTrustedIssuer(iss, list) {
|
||||||
t.Fatal("A bad URL slipped past", iss)
|
t.Fatal("A bad URL slipped past", iss)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -157,7 +147,7 @@ func TestImplicitIssuer(t *testing.T) {
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
iss = "https://example.com/foo"
|
iss = "https://example.com/foo"
|
||||||
if !isTrustedIssuer(iss, nil, r) {
|
if !IsTrustedIssuer(iss, nil, r) {
|
||||||
t.Fatal("A good URL didn't make it:", iss)
|
t.Fatal("A good URL didn't make it:", iss)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -170,7 +160,7 @@ func TestImplicitIssuer(t *testing.T) {
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
iss = "http://example.com/foo"
|
iss = "http://example.com/foo"
|
||||||
if isTrustedIssuer(iss, nil, r) {
|
if IsTrustedIssuer(iss, nil, r) {
|
||||||
t.Fatal("A bad URL slipped past:", iss)
|
t.Fatal("A bad URL slipped past:", iss)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -182,7 +172,7 @@ func TestImplicitIssuer(t *testing.T) {
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
iss = "https://example.com/foo/bar/baz"
|
iss = "https://example.com/foo/bar/baz"
|
||||||
if isTrustedIssuer(iss, nil, r) {
|
if IsTrustedIssuer(iss, nil, r) {
|
||||||
t.Fatal("A bad URL slipped past:", iss)
|
t.Fatal("A bad URL slipped past:", iss)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -194,7 +184,7 @@ func TestImplicitIssuer(t *testing.T) {
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
iss = "https://example.com/"
|
iss = "https://example.com/"
|
||||||
if !isTrustedIssuer(iss, nil, r) {
|
if !IsTrustedIssuer(iss, nil, r) {
|
||||||
t.Fatal("A good URL didn't make it:", iss)
|
t.Fatal("A good URL didn't make it:", iss)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,8 +4,6 @@ package uncached
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"crypto/rsa"
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
@ -14,20 +12,11 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.rootprojects.org/root/keypairs"
|
keypairs "github.com/big-squid/go-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) (JWKMapByID, PublicKeysMap, error) {
|
func OIDCJWKs(baseURL string) (map[string]map[string]string, map[string]keypairs.PublicKey, error) {
|
||||||
baseURL = normalizeBaseURL(baseURL)
|
baseURL = normalizeBaseURL(baseURL)
|
||||||
oidcConf := struct {
|
oidcConf := struct {
|
||||||
JWKSURI string `json:"jwks_uri"`
|
JWKSURI string `json:"jwks_uri"`
|
||||||
@ -48,7 +37,7 @@ func OIDCJWKs(baseURL string) (JWKMapByID, PublicKeysMap, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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) (JWKMapByID, PublicKeysMap, error) {
|
func WellKnownJWKs(baseURL string) (map[string]map[string]string, map[string]keypairs.PublicKey, error) {
|
||||||
baseURL = normalizeBaseURL(baseURL)
|
baseURL = normalizeBaseURL(baseURL)
|
||||||
url := baseURL + ".well-known/jwks.json"
|
url := baseURL + ".well-known/jwks.json"
|
||||||
|
|
||||||
@ -56,9 +45,9 @@ func WellKnownJWKs(baseURL string) (JWKMapByID, PublicKeysMap, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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) (JWKMapByID, PublicKeysMap, error) {
|
func JWKs(jwksurl string) (map[string]map[string]string, map[string]keypairs.PublicKey, error) {
|
||||||
keys := PublicKeysMap{}
|
keys := map[string]keypairs.PublicKey{}
|
||||||
maps := JWKMapByID{}
|
maps := map[string]map[string]string{}
|
||||||
resp := struct {
|
resp := struct {
|
||||||
Keys []map[string]interface{} `json:"keys"`
|
Keys []map[string]interface{} `json:"keys"`
|
||||||
}{
|
}{
|
||||||
@ -77,13 +66,12 @@ func JWKs(jwksurl string) (JWKMapByID, PublicKeysMap, error) {
|
|||||||
k := resp.Keys[i]
|
k := resp.Keys[i]
|
||||||
m := getStringMap(k)
|
m := getStringMap(k)
|
||||||
|
|
||||||
key, err := keypairs.NewJWKPublicKey(m)
|
if key, err := keypairs.NewJWKPublicKey(m); nil != err {
|
||||||
|
|
||||||
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
|
||||||
@ -91,38 +79,32 @@ func JWKs(jwksurl string) (JWKMapByID, PublicKeysMap, error) {
|
|||||||
|
|
||||||
// PEM fetches and parses a PEM (assuming well-known format)
|
// PEM fetches and parses a PEM (assuming well-known format)
|
||||||
func PEM(pemurl string) (map[string]string, keypairs.PublicKey, error) {
|
func PEM(pemurl string) (map[string]string, keypairs.PublicKey, error) {
|
||||||
var pubd keypairs.PublicKeyDeprecated
|
var pub keypairs.PublicKey
|
||||||
if err := safeFetch(pemurl, func(body io.Reader) error {
|
if err := safeFetch(pemurl, func(body io.Reader) error {
|
||||||
pem, err := ioutil.ReadAll(body)
|
pem, err := ioutil.ReadAll(body)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
pubd, err = keypairs.ParsePublicKey(pem)
|
pub, err = keypairs.ParsePublicKey(pem)
|
||||||
if nil != err {
|
return err
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}); nil != err {
|
}); nil != err {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
jwk := map[string]interface{}{}
|
jwk := map[string]interface{}{}
|
||||||
pub := pubd.Key()
|
|
||||||
body := bytes.NewBuffer(keypairs.MarshalJWKPublicKey(pub))
|
body := bytes.NewBuffer(keypairs.MarshalJWKPublicKey(pub))
|
||||||
decoder := json.NewDecoder(body)
|
decoder := json.NewDecoder(body)
|
||||||
decoder.UseNumber()
|
decoder.UseNumber()
|
||||||
_ = decoder.Decode(&jwk)
|
_ = decoder.Decode(&jwk)
|
||||||
|
|
||||||
m := getStringMap(jwk)
|
m := getStringMap(jwk)
|
||||||
m["kid"] = keypairs.Thumbprint(pub)
|
m["kid"] = pemurl
|
||||||
// TODO is this just junk?
|
|
||||||
m[URLishKey] = pemurl
|
|
||||||
|
|
||||||
switch pub.(type) {
|
switch p := pub.(type) {
|
||||||
case *ecdsa.PublicKey:
|
case *keypairs.ECPublicKey:
|
||||||
//p.KID = pemurl
|
p.KID = pemurl
|
||||||
case *rsa.PublicKey:
|
case *keypairs.RSAPublicKey:
|
||||||
//p.KID = pemurl
|
p.KID = pemurl
|
||||||
default:
|
default:
|
||||||
return nil, nil, errors.New("impossible key type")
|
return nil, nil, errors.New("impossible key type")
|
||||||
}
|
}
|
||||||
@ -131,7 +113,7 @@ func PEM(pemurl string) (map[string]string, keypairs.PublicKey, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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.PublicKeyDeprecated, error) {
|
func Fetch(url string) (map[string]string, keypairs.PublicKey, 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)
|
||||||
|
@ -6,7 +6,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.rootprojects.org/root/keypairs"
|
keypairs "github.com/big-squid/go-keypairs"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestJWKs(t *testing.T) {
|
func TestJWKs(t *testing.T) {
|
||||||
|
157
keypairs.go
157
keypairs.go
@ -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,61 +16,32 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"math/big"
|
"math/big"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrInvalidPrivateKey means that the key is not a valid Private Key
|
|
||||||
var ErrInvalidPrivateKey = errors.New("PrivateKey must be of type *rsa.PrivateKey or *ecdsa.PrivateKey")
|
var ErrInvalidPrivateKey = errors.New("PrivateKey must be of type *rsa.PrivateKey or *ecdsa.PrivateKey")
|
||||||
|
|
||||||
// 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")
|
var ErrInvalidPublicKey = errors.New("PublicKey must be of type *rsa.PublicKey or *ecdsa.PublicKey")
|
||||||
|
|
||||||
// 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")
|
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")
|
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")
|
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'")
|
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'")
|
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."
|
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."
|
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 is so that v0.7.x can use golang v1.15 keys
|
// PublicKey thinly veils crypto.PublicKey for type safety
|
||||||
type PublicKey interface {
|
type PublicKey interface {
|
||||||
Equal(x crypto.PublicKey) bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// PublicKeyDeprecated thinly veils crypto.PublicKey for type safety
|
|
||||||
type PublicKeyDeprecated interface {
|
|
||||||
crypto.PublicKey
|
crypto.PublicKey
|
||||||
//Equal(x crypto.PublicKey) bool
|
Thumbprint() string
|
||||||
//Thumbprint() string
|
|
||||||
KeyID() string
|
KeyID() string
|
||||||
Key() PublicKey
|
Key() crypto.PublicKey
|
||||||
ExpiresAt() time.Time
|
ExpiresAt() time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,74 +59,41 @@ type RSAPublicKey struct {
|
|||||||
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 {
|
func (p *ECPublicKey) ExpiresAt() time.Time {
|
||||||
return p.Expiry
|
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 {
|
func (p *RSAPublicKey) ExpiresAt() time.Time {
|
||||||
return p.Expiry
|
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) PublicKeyDeprecated {
|
func NewPublicKey(pub crypto.PublicKey, kid ...string) PublicKey {
|
||||||
_, ok := pub.(PublicKey)
|
var k 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{
|
||||||
@ -177,8 +115,16 @@ func NewPublicKey(pub crypto.PublicKey, kid ...string) PublicKeyDeprecated {
|
|||||||
rsakey.KID = ThumbprintRSAPublicKey(p)
|
rsakey.KID = ThumbprintRSAPublicKey(p)
|
||||||
}
|
}
|
||||||
k = rsakey
|
k = rsakey
|
||||||
|
case *ecdsa.PrivateKey:
|
||||||
|
panic(errors.New(ErrDevSwapPrivatePublic))
|
||||||
|
case *rsa.PrivateKey:
|
||||||
|
panic(errors.New(ErrDevSwapPrivatePublic))
|
||||||
|
case *dsa.PublicKey:
|
||||||
|
panic(ErrInvalidPublicKey)
|
||||||
|
case *dsa.PrivateKey:
|
||||||
|
panic(ErrInvalidPublicKey)
|
||||||
default:
|
default:
|
||||||
panic(fmt.Errorf(ErrDevBadKeyType, pub))
|
panic(errors.New(fmt.Sprintf(ErrDevBadKeyType, pub)))
|
||||||
}
|
}
|
||||||
|
|
||||||
return k
|
return k
|
||||||
@ -188,11 +134,13 @@ func NewPublicKey(pub crypto.PublicKey, kid ...string) PublicKeyDeprecated {
|
|||||||
// making it suitable for use as an OIDC public key.
|
// 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.(type) {
|
switch k := key.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(ErrInvalidPublicKey)
|
||||||
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)
|
||||||
@ -200,13 +148,8 @@ func MarshalJWKPublicKey(key PublicKey, exp ...time.Time) []byte {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Thumbprint returns the SHA256 RFC-spec JWK thumbprint
|
|
||||||
func Thumbprint(pub PublicKey) string {
|
|
||||||
return ThumbprintUntypedPublicKey(pub)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ThumbprintPublicKey returns the SHA256 RFC-spec JWK thumbprint
|
// ThumbprintPublicKey returns the SHA256 RFC-spec JWK thumbprint
|
||||||
func ThumbprintPublicKey(pub PublicKeyDeprecated) string {
|
func ThumbprintPublicKey(pub PublicKey) string {
|
||||||
return ThumbprintUntypedPublicKey(pub.Key())
|
return ThumbprintUntypedPublicKey(pub.Key())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -214,7 +157,7 @@ func ThumbprintPublicKey(pub PublicKeyDeprecated) string {
|
|||||||
// (but will still panic, to help you discover bugs in development rather than production).
|
// (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:
|
case PublicKey:
|
||||||
return ThumbprintUntypedPublicKey(p.Key())
|
return ThumbprintUntypedPublicKey(p.Key())
|
||||||
case *ecdsa.PublicKey:
|
case *ecdsa.PublicKey:
|
||||||
return ThumbprintECPublicKey(p)
|
return ThumbprintECPublicKey(p)
|
||||||
@ -289,20 +232,13 @@ func ParsePrivateKey(block []byte) (PrivateKey, error) {
|
|||||||
|
|
||||||
// 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, ErrParsePrivateKey
|
return nil, ErrParsePrivateKey
|
||||||
}
|
}
|
||||||
@ -373,13 +309,14 @@ func getPEMBytes(block []byte) ([][]byte, error) {
|
|||||||
|
|
||||||
if len(blocks) > 0 {
|
if len(blocks) > 0 {
|
||||||
return blocks, nil
|
return blocks, nil
|
||||||
|
} else {
|
||||||
|
return nil, errors.New("no PEM blocks found")
|
||||||
}
|
}
|
||||||
return nil, errors.New("no PEM blocks found")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParsePublicKey will try to parse the bytes you give it
|
// 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
|
// in any of the supported formats: PEM, DER, PKIX/SPKI, PKCS1, x509 Certificate, and JWK
|
||||||
func ParsePublicKey(block []byte) (PublicKeyDeprecated, error) {
|
func ParsePublicKey(block []byte) (PublicKey, error) {
|
||||||
blocks, err := getPEMBytes(block)
|
blocks, err := getPEMBytes(block)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
return nil, ErrParsePublicKey
|
return nil, ErrParsePublicKey
|
||||||
@ -394,23 +331,16 @@ func ParsePublicKey(block []byte) (PublicKeyDeprecated, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
// If we didn't parse a key arleady, we failed
|
||||||
return nil, ErrParsePublicKey
|
return nil, ErrParsePublicKey
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParsePublicKeyString calls ParsePublicKey([]byte(key)) for all you lazy folk.
|
// ParsePublicKeyString calls ParsePublicKey([]byte(key)) for all you lazy folk.
|
||||||
func ParsePublicKeyString(block string) (PublicKeyDeprecated, error) {
|
func ParsePublicKeyString(block string) (PublicKey, error) {
|
||||||
return ParsePublicKey([]byte(block))
|
return ParsePublicKey([]byte(block))
|
||||||
}
|
}
|
||||||
|
|
||||||
func parsePublicKey(der []byte) (PublicKeyDeprecated, error) {
|
func parsePublicKey(der []byte) (PublicKey, error) {
|
||||||
cert, err := x509.ParseCertificate(der)
|
cert, err := x509.ParseCertificate(der)
|
||||||
if nil == err {
|
if nil == err {
|
||||||
switch k := cert.PublicKey.(type) {
|
switch k := cert.PublicKey.(type) {
|
||||||
@ -456,7 +386,7 @@ func parsePublicKey(der []byte) (PublicKeyDeprecated, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewJWKPublicKey contstructs a PublicKey from the relevant pieces a map[string]string (generic JSON)
|
// NewJWKPublicKey contstructs a PublicKey from the relevant pieces a map[string]string (generic JSON)
|
||||||
func NewJWKPublicKey(m map[string]string) (PublicKeyDeprecated, error) {
|
func NewJWKPublicKey(m map[string]string) (PublicKey, error) {
|
||||||
switch m["kty"] {
|
switch m["kty"] {
|
||||||
case "RSA":
|
case "RSA":
|
||||||
return parseRSAPublicKey(m)
|
return parseRSAPublicKey(m)
|
||||||
@ -468,41 +398,32 @@ func NewJWKPublicKey(m map[string]string) (PublicKeyDeprecated, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ParseJWKPublicKey parses a JSON-encoded JWK and returns a PublicKey, or a (hopefully) helpful error message
|
// ParseJWKPublicKey parses a JSON-encoded JWK and returns a PublicKey, or a (hopefully) helpful error message
|
||||||
func ParseJWKPublicKey(b []byte) (PublicKeyDeprecated, error) {
|
func ParseJWKPublicKey(b []byte) (PublicKey, 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseJWKPublicKeyString calls ParseJWKPublicKey([]byte(key)) for all you lazy folk.
|
// ParseJWKPublicKeyString calls ParseJWKPublicKey([]byte(key)) for all you lazy folk.
|
||||||
func ParseJWKPublicKeyString(s string) (PublicKeyDeprecated, error) {
|
func ParseJWKPublicKeyString(s string) (PublicKey, error) {
|
||||||
if strings.Contains(s, `"d"`) {
|
|
||||||
return nil, ErrUnexpectedPrivateKey
|
|
||||||
}
|
|
||||||
return newJWKPublicKey(s)
|
return newJWKPublicKey(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DecodeJWKPublicKey stream-decodes a JSON-encoded JWK and returns a PublicKey, or a (hopefully) helpful error message
|
// DecodeJWKPublicKey stream-decodes a JSON-encoded JWK and returns a PublicKey, or a (hopefully) helpful error message
|
||||||
func DecodeJWKPublicKey(r io.Reader) (PublicKeyDeprecated, error) {
|
func DecodeJWKPublicKey(r io.Reader) (PublicKey, error) {
|
||||||
m := make(map[string]string)
|
return newJWKPublicKey(r)
|
||||||
if err := json.NewDecoder(r).Decode(&m); nil != err {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if d := m["d"]; "" != d {
|
|
||||||
return nil, ErrUnexpectedPrivateKey
|
|
||||||
}
|
|
||||||
return newJWKPublicKey(m)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// the underpinnings of the parser as used by the typesafe wrappers
|
// the underpinnings of the parser as used by the typesafe wrappers
|
||||||
func newJWKPublicKey(data interface{}) (PublicKeyDeprecated, error) {
|
func newJWKPublicKey(data interface{}) (PublicKey, 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
|
||||||
|
@ -43,53 +43,6 @@ 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",
|
||||||
@ -118,7 +71,7 @@ func TestParsePrivateKeyRSA(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestParseCertificate(t *testing.T) {
|
func TestParseCertificate(t *testing.T) {
|
||||||
resp, err := http.Get("https://example.auth0.com/pem")
|
resp, err := http.Get("http://bigsquid.auth0.com/pem")
|
||||||
if nil != err {
|
if nil != err {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ Basic Usage
|
|||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.rootprojects.org/root/keypairs/keyserve"
|
"github.com/big-squid/go-keypairs/keyserve"
|
||||||
)
|
)
|
||||||
|
|
||||||
key, _ := ecdsa.GenerateKey(elliptic.P256, rand.Reader)
|
key, _ := ecdsa.GenerateKey(elliptic.P256, rand.Reader)
|
||||||
|
@ -11,7 +11,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.rootprojects.org/root/keypairs"
|
keypairs "github.com/big-squid/go-keypairs"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DefaultExpiresIn is 3 days
|
// DefaultExpiresIn is 3 days
|
||||||
@ -39,7 +39,7 @@ var never = time.Time{}
|
|||||||
// Middleware holds your public keys and has http handler methods for OIDC and Auth0 JWKs
|
// Middleware holds your public keys and has http handler methods for OIDC and Auth0 JWKs
|
||||||
type Middleware struct {
|
type Middleware struct {
|
||||||
BaseURL *url.URL
|
BaseURL *url.URL
|
||||||
Keys []keypairs.PublicKeyDeprecated
|
Keys []keypairs.PublicKey
|
||||||
ExpiresIn time.Duration
|
ExpiresIn time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -148,7 +148,7 @@ func (m *Middleware) Auth0PEM(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func marshalJWKs(keys []keypairs.PublicKeyDeprecated, exp2 time.Time) []string {
|
func marshalJWKs(keys []keypairs.PublicKey, exp2 time.Time) []string {
|
||||||
jwks := make([]string, 0, 1)
|
jwks := make([]string, 0, 1)
|
||||||
|
|
||||||
for i := range keys {
|
for i := range keys {
|
||||||
@ -163,8 +163,7 @@ func marshalJWKs(keys []keypairs.PublicKeyDeprecated, exp2 time.Time) []string {
|
|||||||
|
|
||||||
// Note that you don't have to embed `iss` in the JWK because the client
|
// 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.
|
// already has that info by virtue of getting to it in the first place.
|
||||||
pub := key.Key()
|
jwk := string(keypairs.MarshalJWKPublicKey(key, exp))
|
||||||
jwk := string(keypairs.MarshalJWKPublicKey(pub, exp))
|
|
||||||
jwks = append(jwks, jwk)
|
jwks = append(jwks, jwk)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,12 +13,12 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.rootprojects.org/root/keypairs"
|
keypairs "github.com/big-squid/go-keypairs"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestServeKeys(t *testing.T) {
|
func TestServeKeys(t *testing.T) {
|
||||||
eckey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
eckey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
pubs := []keypairs.PublicKeyDeprecated{
|
pubs := []keypairs.PublicKey{
|
||||||
keypairs.NewPublicKey(eckey.Public()),
|
keypairs.NewPublicKey(eckey.Public()),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,9 +42,8 @@ func TestServeKeys(t *testing.T) {
|
|||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
time.Sleep(15 * time.Second)
|
time.Sleep(15 * time.Second)
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
ctx, _ := context.WithTimeout(context.Background(), 15*time.Second)
|
||||||
h.Shutdown(ctx)
|
h.Shutdown(ctx)
|
||||||
cancel()
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
m := map[string]string{}
|
m := map[string]string{}
|
||||||
|
170
marshal.go
170
marshal.go
@ -1,170 +0,0 @@
|
|||||||
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
46
mock.go
@ -1,46 +0,0 @@
|
|||||||
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
165
sign.go
@ -1,165 +0,0 @@
|
|||||||
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
174
verify.go
@ -1,174 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user