Compare commits

..

No commits in common. "3f7513364a5bd7cdce0fa9766850f1ec9c0bc606" and "6a22bfecc40cdba566d2e0619110dc14a8aaf8a0" have entirely different histories.

27 changed files with 51 additions and 878 deletions

2
go.mod
View File

@ -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
View File

@ -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=

View File

@ -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))

View File

@ -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 {

View File

@ -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)
} }

View File

@ -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
} }

View File

@ -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'))

View File

@ -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"`

View File

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

View File

@ -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:'

View File

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

View File

@ -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>.

View File

@ -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

View File

@ -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
}

View File

@ -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()),
}
}
*/

View File

@ -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
}

View File

@ -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,
))
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}
}

2
vendor/modules.txt vendored
View File

@ -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

View File

@ -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