Compare commits
No commits in common. "3f7513364a5bd7cdce0fa9766850f1ec9c0bc606" and "6a22bfecc40cdba566d2e0619110dc14a8aaf8a0" have entirely different histories.
3f7513364a
...
6a22bfecc4
2
go.mod
2
go.mod
|
@ -4,7 +4,7 @@ go 1.13
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.rootprojects.org/root/hashcash v1.0.1
|
git.rootprojects.org/root/hashcash v1.0.1
|
||||||
git.rootprojects.org/root/keypairs v0.6.5
|
git.rootprojects.org/root/keypairs v0.5.2
|
||||||
github.com/google/uuid v1.1.1
|
github.com/google/uuid v1.1.1
|
||||||
github.com/joho/godotenv v1.3.0
|
github.com/joho/godotenv v1.3.0
|
||||||
github.com/mailgun/mailgun-go/v3 v3.6.4
|
github.com/mailgun/mailgun-go/v3 v3.6.4
|
||||||
|
|
4
go.sum
4
go.sum
|
@ -1,7 +1,7 @@
|
||||||
git.rootprojects.org/root/hashcash v1.0.1 h1:PkzwZu4CR5q/hwAntJdvcmNhmP0ONhetMo7rYhIZhZ0=
|
git.rootprojects.org/root/hashcash v1.0.1 h1:PkzwZu4CR5q/hwAntJdvcmNhmP0ONhetMo7rYhIZhZ0=
|
||||||
git.rootprojects.org/root/hashcash v1.0.1/go.mod h1:HdoULUe94o1NVMES5K6aP3p8QGQiIia73F1HNZ1+FkQ=
|
git.rootprojects.org/root/hashcash v1.0.1/go.mod h1:HdoULUe94o1NVMES5K6aP3p8QGQiIia73F1HNZ1+FkQ=
|
||||||
git.rootprojects.org/root/keypairs v0.6.5 h1:sdRAQD/O/JBS8+ZxUewXnY+cjQVDNH3TmcS+KtANZqA=
|
git.rootprojects.org/root/keypairs v0.5.2 h1:jr+drUUm/REaCDJTl5gT3kF2PwlXygcLsBZlqoKTZZw=
|
||||||
git.rootprojects.org/root/keypairs v0.6.5/go.mod h1:WGI8PadOp+4LjUuI+wNlSwcJwFtY8L9XuNjuO3213HA=
|
git.rootprojects.org/root/keypairs v0.5.2/go.mod h1:WGI8PadOp+4LjUuI+wNlSwcJwFtY8L9XuNjuO3213HA=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 h1:0JZ+dUmQeA8IIVUMzysrX4/AKuQwWhV2dYQuPZdvdSQ=
|
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 h1:0JZ+dUmQeA8IIVUMzysrX4/AKuQwWhV2dYQuPZdvdSQ=
|
||||||
|
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.coolaj86.com/coolaj86/go-mockid/mockid"
|
"git.coolaj86.com/coolaj86/go-mockid/mockid"
|
||||||
|
"git.coolaj86.com/coolaj86/go-mockid/xkeypairs"
|
||||||
"git.rootprojects.org/root/keypairs"
|
"git.rootprojects.org/root/keypairs"
|
||||||
|
|
||||||
_ "github.com/joho/godotenv/autoload"
|
_ "github.com/joho/godotenv/autoload"
|
||||||
|
@ -91,7 +92,7 @@ func main() {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// TODO privB := keypairs.MarshalJWKPrivateKey(privkey)
|
// TODO privB := keypairs.MarshalJWKPrivateKey(privkey)
|
||||||
privB := keypairs.MarshalJWKPrivateKey(privkey)
|
privB := xkeypairs.MarshalJWKPrivateKey(privkey)
|
||||||
fmt.Printf("Private Key:\n\t%s\n", string(privB))
|
fmt.Printf("Private Key:\n\t%s\n", string(privB))
|
||||||
pubB := keypairs.MarshalJWKPublicKey(keypairs.NewPublicKey(privkey.Public()))
|
pubB := keypairs.MarshalJWKPublicKey(keypairs.NewPublicKey(privkey.Public()))
|
||||||
fmt.Printf("Public Key:\n\t%s\n", string(pubB))
|
fmt.Printf("Public Key:\n\t%s\n", string(pubB))
|
||||||
|
|
|
@ -13,7 +13,6 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.coolaj86.com/coolaj86/go-mockid/xkeypairs"
|
"git.coolaj86.com/coolaj86/go-mockid/xkeypairs"
|
||||||
"git.rootprojects.org/root/keypairs"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -48,8 +47,8 @@ func getOpts(r *http.Request) (*xkeypairs.KeyOptions, error) {
|
||||||
Key: key,
|
Key: key,
|
||||||
}
|
}
|
||||||
|
|
||||||
opts.Claims, _ = tok["claims"].(keypairs.Object)
|
opts.Claims, _ = tok["claims"].(xkeypairs.Object)
|
||||||
opts.Header, _ = tok["header"].(keypairs.Object)
|
opts.Header, _ = tok["header"].(xkeypairs.Object)
|
||||||
|
|
||||||
var n int
|
var n int
|
||||||
if 0 != seed {
|
if 0 != seed {
|
||||||
|
|
|
@ -45,7 +45,7 @@ func GeneratePrivateJWK(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
privkey := xkeypairs.GenPrivKey(opts)
|
privkey := xkeypairs.GenPrivKey(opts)
|
||||||
|
|
||||||
jwk := keypairs.MarshalJWKPrivateKey(privkey)
|
jwk := xkeypairs.MarshalJWKPrivateKey(privkey)
|
||||||
w.Write(append(jwk, '\n'))
|
w.Write(append(jwk, '\n'))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,7 +68,7 @@ func GeneratePublicDER(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
b, _ := keypairs.MarshalDERPublicKey(privkey.Public())
|
b, _ := xkeypairs.MarshalDERPublicKey(privkey.Public())
|
||||||
|
|
||||||
w.Write(b)
|
w.Write(b)
|
||||||
}
|
}
|
||||||
|
@ -88,7 +88,7 @@ func GeneratePrivateDER(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
privkey := xkeypairs.GenPrivKey(opts)
|
privkey := xkeypairs.GenPrivKey(opts)
|
||||||
|
|
||||||
der, _ := keypairs.MarshalDERPrivateKey(privkey)
|
der, _ := xkeypairs.MarshalDERPrivateKey(privkey)
|
||||||
w.Write(der)
|
w.Write(der)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -111,7 +111,7 @@ func GeneratePublicPEM(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
b, _ := keypairs.MarshalPEMPublicKey(privkey.Public())
|
b, _ := xkeypairs.MarshalPEMPublicKey(privkey.Public())
|
||||||
|
|
||||||
w.Write(b)
|
w.Write(b)
|
||||||
}
|
}
|
||||||
|
@ -131,7 +131,7 @@ func GeneratePrivatePEM(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
privkey := xkeypairs.GenPrivKey(opts)
|
privkey := xkeypairs.GenPrivKey(opts)
|
||||||
|
|
||||||
privpem, _ := keypairs.MarshalPEMPrivateKey(privkey)
|
privpem, _ := xkeypairs.MarshalPEMPrivateKey(privkey)
|
||||||
w.Write(privpem)
|
w.Write(privpem)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.rootprojects.org/root/keypairs"
|
"git.coolaj86.com/coolaj86/go-mockid/xkeypairs"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SignJWS will create an uncompressed JWT with the given payload
|
// SignJWS will create an uncompressed JWT with the given payload
|
||||||
|
@ -40,7 +40,7 @@ func sign(w http.ResponseWriter, r *http.Request, jwt bool) {
|
||||||
header["_seed"] = opts.Seed
|
header["_seed"] = opts.Seed
|
||||||
}
|
}
|
||||||
|
|
||||||
jws, err := keypairs.SignClaims(privkey, header, opts.Claims)
|
jws, err := xkeypairs.SignClaims(privkey, header, opts.Claims)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
|
@ -48,7 +48,7 @@ func sign(w http.ResponseWriter, r *http.Request, jwt bool) {
|
||||||
|
|
||||||
var b []byte
|
var b []byte
|
||||||
if jwt {
|
if jwt {
|
||||||
s := keypairs.JWSToJWT(jws)
|
s := xkeypairs.JWSToJWT(jws)
|
||||||
w.Write(append([]byte(s), '\n'))
|
w.Write(append([]byte(s), '\n'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.rootprojects.org/root/keypairs"
|
"git.coolaj86.com/coolaj86/go-mockid/xkeypairs"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Verify will verify both JWT and uncompressed JWS
|
// Verify will verify both JWT and uncompressed JWS
|
||||||
|
@ -19,7 +19,7 @@ func Verify(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
jws := &keypairs.JWS{}
|
jws := &xkeypairs.JWS{}
|
||||||
|
|
||||||
authzParts := strings.Split(r.Header.Get("Authorization"), " ")
|
authzParts := strings.Split(r.Header.Get("Authorization"), " ")
|
||||||
lenAuthz := len(authzParts)
|
lenAuthz := len(authzParts)
|
||||||
|
@ -75,12 +75,16 @@ func Verify(w http.ResponseWriter, r *http.Request) {
|
||||||
jws.Claims["exp"] = float64(time.Now().Add(5 * time.Minute).Unix())
|
jws.Claims["exp"] = float64(time.Now().Add(5 * time.Minute).Unix())
|
||||||
}
|
}
|
||||||
|
|
||||||
errs := keypairs.VerifyClaims(nil, jws)
|
ok, err := xkeypairs.VerifyClaims(nil, jws)
|
||||||
if 0 == len(errs) {
|
if nil != err {
|
||||||
log.Printf("jws verify error: %s", errs)
|
log.Printf("jws verify error: %s", err)
|
||||||
http.Error(w, "Bad Request: could not verify JWS claims", http.StatusBadRequest)
|
http.Error(w, "Bad Request: could not verify JWS claims", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if !ok {
|
||||||
|
http.Error(w, "Bad Request: invalid JWS signature", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
b := []byte(`{"success":true}`)
|
b := []byte(`{"success":true}`)
|
||||||
w.Write(append(b, '\n'))
|
w.Write(append(b, '\n'))
|
||||||
|
|
|
@ -66,13 +66,11 @@ type OTPResponse struct {
|
||||||
HTTPResponse
|
HTTPResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
// Contact represents a map between an identifier and some users
|
|
||||||
type Contact struct {
|
type Contact struct {
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Subjects []string `json:"subjects"`
|
Subjects []string `json:"subjects"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subject represents a map between a user and some identifiers
|
|
||||||
type Subject struct {
|
type Subject struct {
|
||||||
Subject string `json:"subject"`
|
Subject string `json:"subject"`
|
||||||
Emails map[string]time.Time `json:"emails"`
|
Emails map[string]time.Time `json:"emails"`
|
||||||
|
@ -111,7 +109,7 @@ func Route(jwksPrefix string, privkey keypairs.PrivateKey) http.Handler {
|
||||||
if nil != err {
|
if nil != err {
|
||||||
signingKey = xkeypairs.GenPrivKey(&xkeypairs.KeyOptions{})
|
signingKey = xkeypairs.GenPrivKey(&xkeypairs.KeyOptions{})
|
||||||
_ = os.MkdirAll(jwksPrefix+"/private", 0750)
|
_ = os.MkdirAll(jwksPrefix+"/private", 0750)
|
||||||
b := keypairs.MarshalJWKPrivateKey(signingKey)
|
b := xkeypairs.MarshalJWKPrivateKey(signingKey)
|
||||||
if err := ioutil.WriteFile(privKeyJWKPath, b, 0600); nil != err {
|
if err := ioutil.WriteFile(privKeyJWKPath, b, 0600); nil != err {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
@ -322,10 +320,10 @@ func Route(jwksPrefix string, privkey keypairs.PrivateKey) http.Handler {
|
||||||
uuid, _ := uuid.NewRandom()
|
uuid, _ := uuid.NewRandom()
|
||||||
nonce, _ := uuid.MarshalBinary()
|
nonce, _ := uuid.MarshalBinary()
|
||||||
baseURL := getBaseURL(r)
|
baseURL := getBaseURL(r)
|
||||||
tok, err := keypairs.SignClaims(
|
tok, err := xkeypairs.SignClaims(
|
||||||
signingKey,
|
signingKey,
|
||||||
keypairs.Object{},
|
xkeypairs.Object{},
|
||||||
keypairs.Object{
|
xkeypairs.Object{
|
||||||
"sub": sub,
|
"sub": sub,
|
||||||
"iss": baseURL + "/",
|
"iss": baseURL + "/",
|
||||||
"jti": base64.RawURLEncoding.EncodeToString(nonce),
|
"jti": base64.RawURLEncoding.EncodeToString(nonce),
|
||||||
|
@ -338,7 +336,7 @@ func Route(jwksPrefix string, privkey keypairs.PrivateKey) http.Handler {
|
||||||
fmt.Fprintf(w, "%s", err)
|
fmt.Fprintf(w, "%s", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
otp.AccessToken = keypairs.JWSToJWT(tok)
|
otp.AccessToken = xkeypairs.JWSToJWT(tok)
|
||||||
b, _ := json.Marshal(&OTPResponse{
|
b, _ := json.Marshal(&OTPResponse{
|
||||||
HTTPResponse: HTTPResponse{Success: true},
|
HTTPResponse: HTTPResponse{Success: true},
|
||||||
OTP: *otp,
|
OTP: *otp,
|
||||||
|
@ -463,7 +461,7 @@ func Route(jwksPrefix string, privkey keypairs.PrivateKey) http.Handler {
|
||||||
http.HandleFunc("/api/new-account", func(w http.ResponseWriter, r *http.Request) {
|
http.HandleFunc("/api/new-account", func(w http.ResponseWriter, r *http.Request) {
|
||||||
myURL := getBaseURL(r) + r.URL.Path
|
myURL := getBaseURL(r) + r.URL.Path
|
||||||
|
|
||||||
jws := &keypairs.JWS{}
|
jws := &xkeypairs.JWS{}
|
||||||
|
|
||||||
decoder := json.NewDecoder(r.Body)
|
decoder := json.NewDecoder(r.Body)
|
||||||
if err := decoder.Decode(jws); nil != err {
|
if err := decoder.Decode(jws); nil != err {
|
||||||
|
@ -506,8 +504,11 @@ func Route(jwksPrefix string, privkey keypairs.PrivateKey) http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
errs := keypairs.VerifyClaims(nil, jws)
|
ok, err := xkeypairs.VerifyClaims(nil, jws)
|
||||||
if 0 != len(errs) {
|
if nil != err || !ok {
|
||||||
|
if nil != err {
|
||||||
|
log.Printf("jws verify error: %s", err)
|
||||||
|
}
|
||||||
http.Error(w, "Bad Request", http.StatusBadRequest)
|
http.Error(w, "Bad Request", http.StatusBadRequest)
|
||||||
fmt.Fprintf(w, `{"error":"could not verify JWS claims"}`+"\n")
|
fmt.Fprintf(w, `{"error":"could not verify JWS claims"}`+"\n")
|
||||||
return
|
return
|
||||||
|
@ -529,7 +530,7 @@ func Route(jwksPrefix string, privkey keypairs.PrivateKey) http.Handler {
|
||||||
)
|
)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
http.Error(w, "Bad Request", http.StatusBadRequest)
|
http.Error(w, "Bad Request", http.StatusBadRequest)
|
||||||
msg, _ := json.Marshal(err)
|
msg, _ := json.Marshal(err.Error())
|
||||||
fmt.Fprintf(w, `{"error":%s}`+"\n", msg)
|
fmt.Fprintf(w, `{"error":%s}`+"\n", msg)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -683,7 +684,7 @@ func Route(jwksPrefix string, privkey keypairs.PrivateKey) http.Handler {
|
||||||
|
|
||||||
http.HandleFunc("/key.jwk.json", func(w http.ResponseWriter, r *http.Request) {
|
http.HandleFunc("/key.jwk.json", func(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Printf("%s %s", r.Method, r.URL.Path)
|
log.Printf("%s %s", r.Method, r.URL.Path)
|
||||||
jwk := string(keypairs.MarshalJWKPrivateKey(privkey))
|
jwk := string(xkeypairs.MarshalJWKPrivateKey(privkey))
|
||||||
jwk = strings.Replace(jwk, `{"`, `{ "`, 1)
|
jwk = strings.Replace(jwk, `{"`, `{ "`, 1)
|
||||||
jwk = strings.Replace(jwk, `",`, `", `, -1)
|
jwk = strings.Replace(jwk, `",`, `", `, -1)
|
||||||
jwk = jwk[0 : len(jwk)-1]
|
jwk = jwk[0 : len(jwk)-1]
|
||||||
|
@ -815,7 +816,6 @@ func verifyToken(token string) (*InspectableToken, error) {
|
||||||
return inspected, nil
|
return inspected, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// OTP is the one-time password for auth
|
|
||||||
type OTP struct {
|
type OTP struct {
|
||||||
//Attempts int `json:"attempts"`
|
//Attempts int `json:"attempts"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
|
|
@ -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:'
|
|
|
@ -1 +0,0 @@
|
||||||
AJ ONeal <aj@therootcompany.com> (https://therootcompany.com)
|
|
|
@ -1,4 +1,4 @@
|
||||||
# [keypairs](https://git.rootprojects.org/root/keypairs)
|
# go-keypairs
|
||||||
|
|
||||||
JSON Web Key (JWK) support and type safety lightly placed over top of Go's `crypto/ecdsa` and `crypto/rsa`
|
JSON Web Key (JWK) support and type safety lightly placed over top of Go's `crypto/ecdsa` and `crypto/rsa`
|
||||||
|
|
||||||
|
@ -14,9 +14,9 @@ jwk, err := keypairs.MarshalJWKPublicKey(pub, time.Now().Add(2 * time.Day))
|
||||||
kid, err := keypairs.ThumbprintPublicKey(pub)
|
kid, err := keypairs.ThumbprintPublicKey(pub)
|
||||||
```
|
```
|
||||||
|
|
||||||
# GoDoc API Documentation
|
# API Documentation
|
||||||
|
|
||||||
See <https://pkg.go.dev/git.rootprojects.org/root/keypairs>
|
See <https://godoc.org/github.com/big-squid/go-keypairs>
|
||||||
|
|
||||||
# Philosophy
|
# Philosophy
|
||||||
|
|
||||||
|
@ -56,8 +56,8 @@ between the ASN.1, x509, PEM, and JWK formats.
|
||||||
|
|
||||||
# LICENSE
|
# LICENSE
|
||||||
|
|
||||||
Copyright (c) 2020-present AJ ONeal \
|
Copyright (c) 2020-present AJ ONeal
|
||||||
Copyright (c) 2018-2019 Big Squid, Inc.
|
Copyright (c) 2018-2019 Big Squid, Inc.
|
||||||
|
|
||||||
This work is licensed under the terms of the MIT license. \
|
This work is licensed under the terms of the MIT license.
|
||||||
For a copy, see <https://opensource.org/licenses/MIT>.
|
For a copy, see <https://opensource.org/licenses/MIT>.
|
||||||
|
|
|
@ -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,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
|
|
||||||
}
|
|
|
@ -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()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -1,171 +0,0 @@
|
||||||
package keypairs
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto"
|
|
||||||
"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 crypto.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 crypto.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 crypto.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,
|
|
||||||
))
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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(NewPublicKey(privkey.Public()), header)
|
|
||||||
if nil != err {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
protected64 := base64.RawURLEncoding.EncodeToString(protected)
|
|
||||||
|
|
||||||
payload, err := claimsToPayload(claims)
|
|
||||||
if nil != err {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
payload64 := base64.RawURLEncoding.EncodeToString(payload)
|
|
||||||
|
|
||||||
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.Key().(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(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
|
|
||||||
}
|
|
|
@ -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 PublicKey = nil
|
|
||||||
log.Println("Security TODO: did not check jws.Claims[\"sub\"] against 'jwk'")
|
|
||||||
log.Println("Security TODO: did not check jws.Claims[\"iss\"]")
|
|
||||||
kty := jwkmap["kty"]
|
|
||||||
var err error
|
|
||||||
if "RSA" == kty {
|
|
||||||
e, _ := jwkmap["e"].(string)
|
|
||||||
n, _ := jwkmap["n"].(string)
|
|
||||||
k, _ := (&RSAJWK{
|
|
||||||
Exp: e,
|
|
||||||
N: n,
|
|
||||||
}).marshalJWK()
|
|
||||||
pub, err = ParseJWKPublicKey(k)
|
|
||||||
if nil != err {
|
|
||||||
return nil, append(errs, err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
crv, _ := jwkmap["crv"].(string)
|
|
||||||
x, _ := jwkmap["x"].(string)
|
|
||||||
y, _ := jwkmap["y"].(string)
|
|
||||||
k, _ := (&ECJWK{
|
|
||||||
Curve: crv,
|
|
||||||
X: x,
|
|
||||||
Y: y,
|
|
||||||
}).marshalJWK()
|
|
||||||
pub, err = ParseJWKPublicKey(k)
|
|
||||||
if nil != err {
|
|
||||||
return nil, append(errs, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return pub, errs
|
|
||||||
}
|
|
||||||
|
|
||||||
func pubkeyCheck(pubkey PublicKey, kid string, opts *keyOptions, errs []error) (PublicKey, []error) {
|
|
||||||
var pub PublicKey = nil
|
|
||||||
|
|
||||||
if "" == kid {
|
|
||||||
err := errors.New("token should have 'kid' or 'jwk' in header to identify the public key")
|
|
||||||
errs = append(errs, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if nil == pubkey {
|
|
||||||
if allowMocking {
|
|
||||||
if 0 == opts.mockSeed {
|
|
||||||
err := errors.New("the debug API requires '_seed' to accompany 'kid'")
|
|
||||||
errs = append(errs, err)
|
|
||||||
}
|
|
||||||
if "" == opts.KeyType {
|
|
||||||
err := errors.New("the debug API requires '_kty' to accompany '_seed'")
|
|
||||||
errs = append(errs, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if 0 == opts.mockSeed || "" == opts.KeyType {
|
|
||||||
return nil, errs
|
|
||||||
}
|
|
||||||
privkey := newPrivateKey(opts)
|
|
||||||
pub = NewPublicKey(privkey.Public())
|
|
||||||
return pub, errs
|
|
||||||
}
|
|
||||||
err := errors.New("no matching public key")
|
|
||||||
errs = append(errs, err)
|
|
||||||
} else {
|
|
||||||
pub = pubkey
|
|
||||||
}
|
|
||||||
|
|
||||||
if nil != pub && "" != kid {
|
|
||||||
if 1 != subtle.ConstantTimeCompare([]byte(kid), []byte(pub.Thumbprint())) {
|
|
||||||
err := errors.New("'kid' does not match the public key thumbprint")
|
|
||||||
errs = append(errs, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return pub, errs
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify will check the signature of a hash
|
|
||||||
func Verify(pubkey PublicKey, hash []byte, sig []byte) bool {
|
|
||||||
|
|
||||||
switch pub := pubkey.Key().(type) {
|
|
||||||
case *rsa.PublicKey:
|
|
||||||
//log.Printf("RSA VERIFY")
|
|
||||||
// TODO Size(key) to detect key size ?
|
|
||||||
//alg := "SHA256"
|
|
||||||
// TODO: this hasn't been tested yet
|
|
||||||
if err := rsa.VerifyPKCS1v15(pub, crypto.SHA256, hash, sig); nil != err {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
case *ecdsa.PublicKey:
|
|
||||||
r := &big.Int{}
|
|
||||||
r.SetBytes(sig[0:32])
|
|
||||||
s := &big.Int{}
|
|
||||||
s.SetBytes(sig[32:])
|
|
||||||
return ecdsa.Verify(pub, hash, r, s)
|
|
||||||
default:
|
|
||||||
panic("impossible condition: non-rsa/non-ecdsa key")
|
|
||||||
//return false
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +1,6 @@
|
||||||
# git.rootprojects.org/root/hashcash v1.0.1
|
# git.rootprojects.org/root/hashcash v1.0.1
|
||||||
git.rootprojects.org/root/hashcash
|
git.rootprojects.org/root/hashcash
|
||||||
# git.rootprojects.org/root/keypairs v0.6.5
|
# git.rootprojects.org/root/keypairs v0.5.2
|
||||||
git.rootprojects.org/root/keypairs
|
git.rootprojects.org/root/keypairs
|
||||||
git.rootprojects.org/root/keypairs/keyfetch
|
git.rootprojects.org/root/keypairs/keyfetch
|
||||||
git.rootprojects.org/root/keypairs/keyfetch/uncached
|
git.rootprojects.org/root/keypairs/keyfetch/uncached
|
||||||
|
|
|
@ -3,30 +3,22 @@ package xkeypairs
|
||||||
import (
|
import (
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
"crypto/elliptic"
|
"crypto/elliptic"
|
||||||
"crypto/rand"
|
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
mathrand "math/rand"
|
"math/rand"
|
||||||
|
|
||||||
"git.rootprojects.org/root/keypairs"
|
"git.rootprojects.org/root/keypairs"
|
||||||
)
|
)
|
||||||
|
|
||||||
const maxRetry = 16
|
|
||||||
|
|
||||||
// RandomReader may be overwritten for testing
|
|
||||||
var RandomReader io.Reader = rand.Reader
|
|
||||||
|
|
||||||
//var RandomReader = rand.Reader
|
|
||||||
|
|
||||||
// KeyOptions are the things that we may need to know about a request to fulfill it properly
|
// KeyOptions are the things that we may need to know about a request to fulfill it properly
|
||||||
type KeyOptions struct {
|
type KeyOptions struct {
|
||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
KeyType string `json:"kty"`
|
KeyType string `json:"kty"`
|
||||||
Seed int64 `json:"-"`
|
Seed int64 `json:"-"`
|
||||||
SeedStr string `json:"seed"`
|
SeedStr string `json:"seed"`
|
||||||
Claims keypairs.Object `json:"claims"`
|
Claims Object `json:"claims"`
|
||||||
Header keypairs.Object `json:"header"`
|
Header Object `json:"header"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// this shananigans is only for testing and debug API stuff
|
// this shananigans is only for testing and debug API stuff
|
||||||
|
@ -34,7 +26,7 @@ func (o *KeyOptions) MyFooNextReader() io.Reader {
|
||||||
if 0 == o.Seed {
|
if 0 == o.Seed {
|
||||||
return RandomReader
|
return RandomReader
|
||||||
}
|
}
|
||||||
return mathrand.New(mathrand.NewSource(o.Seed))
|
return rand.New(rand.NewSource(o.Seed))
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenPrivKey generates a 256-bit entropy RSA or ECDSA private key
|
// GenPrivKey generates a 256-bit entropy RSA or ECDSA private key
|
||||||
|
|
Loading…
Reference in New Issue