diff --git a/go.mod b/go.mod index a325ddb..aaf1a96 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.13 require ( git.rootprojects.org/root/hashcash v1.0.1 - git.rootprojects.org/root/keypairs v0.5.2 + git.rootprojects.org/root/keypairs v0.6.5 github.com/google/uuid v1.1.1 github.com/joho/godotenv v1.3.0 github.com/mailgun/mailgun-go/v3 v3.6.4 diff --git a/go.sum b/go.sum index bf5a355..bb9ba55 100644 --- a/go.sum +++ b/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/go.mod h1:HdoULUe94o1NVMES5K6aP3p8QGQiIia73F1HNZ1+FkQ= -git.rootprojects.org/root/keypairs v0.5.2 h1:jr+drUUm/REaCDJTl5gT3kF2PwlXygcLsBZlqoKTZZw= -git.rootprojects.org/root/keypairs v0.5.2/go.mod h1:WGI8PadOp+4LjUuI+wNlSwcJwFtY8L9XuNjuO3213HA= +git.rootprojects.org/root/keypairs v0.6.5 h1:sdRAQD/O/JBS8+ZxUewXnY+cjQVDNH3TmcS+KtANZqA= +git.rootprojects.org/root/keypairs v0.6.5/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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 h1:0JZ+dUmQeA8IIVUMzysrX4/AKuQwWhV2dYQuPZdvdSQ= diff --git a/vendor/git.rootprojects.org/root/keypairs/.gitignore b/vendor/git.rootprojects.org/root/keypairs/.gitignore new file mode 100644 index 0000000..9140b88 --- /dev/null +++ b/vendor/git.rootprojects.org/root/keypairs/.gitignore @@ -0,0 +1,5 @@ +/keypairs +/dist/ + +.DS_Store +.*.sw* diff --git a/vendor/git.rootprojects.org/root/keypairs/.goreleaser.yml b/vendor/git.rootprojects.org/root/keypairs/.goreleaser.yml new file mode 100644 index 0000000..9b6df83 --- /dev/null +++ b/vendor/git.rootprojects.org/root/keypairs/.goreleaser.yml @@ -0,0 +1,41 @@ +# This is an example goreleaser.yaml file with some sane defaults. +# Make sure to check the documentation at http://goreleaser.com +before: + hooks: + - go generate ./... +builds: + - id: keypairs + main: ./cmd/keypairs/keypairs.go + env: + - CGO_ENABLED=0 + flags: + - -mod=vendor + goos: + - linux + - windows + - darwin + - freebsd + goarch: + - amd64 + - arm + - arm64 +archives: + - replacements: + 386: i386 + amd64: x86-64 + arm64: aarch64 + format_overrides: + - goos: windows + format: zip +env_files: + github_token: ~/.config/goreleaser/github_token.txt +checksum: + name_template: 'checksums.txt' +snapshot: + name_template: "{{ .Tag }}-next" +changelog: + sort: asc + filters: + exclude: + - '^docs:' + - '^test:' diff --git a/vendor/git.rootprojects.org/root/keypairs/AUTHORS b/vendor/git.rootprojects.org/root/keypairs/AUTHORS new file mode 100644 index 0000000..12d2230 --- /dev/null +++ b/vendor/git.rootprojects.org/root/keypairs/AUTHORS @@ -0,0 +1 @@ +AJ ONeal (https://therootcompany.com) diff --git a/vendor/git.rootprojects.org/root/keypairs/README.md b/vendor/git.rootprojects.org/root/keypairs/README.md index d9a5a5c..f020b23 100644 --- a/vendor/git.rootprojects.org/root/keypairs/README.md +++ b/vendor/git.rootprojects.org/root/keypairs/README.md @@ -1,4 +1,4 @@ -# go-keypairs +# [keypairs](https://git.rootprojects.org/root/keypairs) 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) ``` -# API Documentation +# GoDoc API Documentation -See +See # Philosophy @@ -56,8 +56,8 @@ between the ASN.1, x509, PEM, and JWK formats. # LICENSE -Copyright (c) 2020-present AJ ONeal +Copyright (c) 2020-present AJ ONeal \ 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 . diff --git a/vendor/git.rootprojects.org/root/keypairs/cli_test.sh b/vendor/git.rootprojects.org/root/keypairs/cli_test.sh new file mode 100644 index 0000000..6420e26 --- /dev/null +++ b/vendor/git.rootprojects.org/root/keypairs/cli_test.sh @@ -0,0 +1,19 @@ +#!/bin/bash +set -u + +go build -mod=vendor cmd/keypairs/*.go +./keypairs gen > testkey.jwk.json 2> testpub.jwk.json + +./keypairs sign --exp 1h ./testkey.jwk.json '{"foo":"bar"}' > testjwt.txt 2> testjws.json + +echo "" +echo "Should pass:" +./keypairs verify ./testpub.jwk.json testjwt.txt > /dev/null +./keypairs verify ./testpub.jwk.json "$(cat testjwt.txt)" > /dev/null +./keypairs verify ./testpub.jwk.json testjws.json > /dev/null +./keypairs verify ./testpub.jwk.json "$(cat testjws.json)" > /dev/null + +echo "" +echo "Should fail:" +./keypairs sign --exp -1m ./testkey.jwk.json '{"bar":"foo"}' > errjwt.txt 2> errjws.json +./keypairs verify ./testpub.jwk.json errjwt.txt > /dev/null diff --git a/vendor/git.rootprojects.org/root/keypairs/generate.go b/vendor/git.rootprojects.org/root/keypairs/generate.go new file mode 100644 index 0000000..13f99ec --- /dev/null +++ b/vendor/git.rootprojects.org/root/keypairs/generate.go @@ -0,0 +1,69 @@ +package keypairs + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "io" + mathrand "math/rand" + "time" +) + +var randReader io.Reader = rand.Reader +var allowMocking = false + +// KeyOptions are the things that we may need to know about a request to fulfill it properly +type keyOptions struct { + //Key string `json:"key"` + KeyType string `json:"kty"` + mockSeed int64 //`json:"-"` + //SeedStr string `json:"seed"` + //Claims Object `json:"claims"` + //Header Object `json:"header"` +} + +func (o *keyOptions) nextReader() io.Reader { + if allowMocking { + return o.maybeMockReader() + } + return randReader +} + +// NewDefaultPrivateKey generates a key with reasonable strength. +// Today that means a 256-bit equivalent - either RSA 2048 or EC P-256. +func NewDefaultPrivateKey() PrivateKey { + // insecure random is okay here, + // it's just used for a coin toss + mathrand.Seed(time.Now().UnixNano()) + coin := mathrand.Int() + + // the idea here is that we want to make + // it dead simple to support RSA and EC + // so it shouldn't matter which is used + if 0 == coin%2 { + return newPrivateKey(&keyOptions{ + KeyType: "RSA", + }) + } + return newPrivateKey(&keyOptions{ + KeyType: "EC", + }) +} + +// newPrivateKey generates a 256-bit entropy RSA or ECDSA private key +func newPrivateKey(opts *keyOptions) PrivateKey { + var privkey PrivateKey + + if "RSA" == opts.KeyType { + keylen := 2048 + privkey, _ = rsa.GenerateKey(opts.nextReader(), keylen) + if allowMocking { + privkey = maybeDerandomizeMockKey(privkey, keylen, opts) + } + } else { + // TODO: EC keys may also suffer the same random problems in the future + privkey, _ = ecdsa.GenerateKey(elliptic.P256(), opts.nextReader()) + } + return privkey +} diff --git a/vendor/git.rootprojects.org/root/keypairs/jwk.go b/vendor/git.rootprojects.org/root/keypairs/jwk.go new file mode 100644 index 0000000..2149fa6 --- /dev/null +++ b/vendor/git.rootprojects.org/root/keypairs/jwk.go @@ -0,0 +1,69 @@ +package keypairs + +import ( + "fmt" +) + +// JWK abstracts EC and RSA keys +type JWK interface { + marshalJWK() ([]byte, error) +} + +// ECJWK is the EC variant +type ECJWK struct { + KeyID string `json:"kid,omitempty"` + Curve string `json:"crv"` + X string `json:"x"` + Y string `json:"y"` + Use []string `json:"use,omitempty"` + Seed string `json:"_seed,omitempty"` +} + +func (k *ECJWK) marshalJWK() ([]byte, error) { + return []byte(fmt.Sprintf(`{"crv":%q,"kty":"EC","x":%q,"y":%q}`, k.Curve, k.X, k.Y)), nil +} + +// RSAJWK is the RSA variant +type RSAJWK struct { + KeyID string `json:"kid,omitempty"` + Exp string `json:"e"` + N string `json:"n"` + Use []string `json:"use,omitempty"` + Seed string `json:"_seed,omitempty"` +} + +func (k *RSAJWK) marshalJWK() ([]byte, error) { + return []byte(fmt.Sprintf(`{"e":%q,"kty":"RSA","n":%q}`, k.Exp, k.N)), nil +} + +/* +// ToPublicJWK exposes only the public parts +func ToPublicJWK(pubkey PublicKey) JWK { + switch k := pubkey.Key().(type) { + case *ecdsa.PublicKey: + return ECToPublicJWK(k) + case *rsa.PublicKey: + return RSAToPublicJWK(k) + default: + panic(errors.New("impossible key type")) + //return nil + } +} + +// ECToPublicJWK will output the most minimal version of an EC JWK (no key id, no "use" flag, nada) +func ECToPublicJWK(k *ecdsa.PublicKey) *ECJWK { + return &ECJWK{ + Curve: k.Curve.Params().Name, + X: base64.RawURLEncoding.EncodeToString(k.X.Bytes()), + Y: base64.RawURLEncoding.EncodeToString(k.Y.Bytes()), + } +} + +// RSAToPublicJWK will output the most minimal version of an RSA JWK (no key id, no "use" flag, nada) +func RSAToPublicJWK(p *rsa.PublicKey) *RSAJWK { + return &RSAJWK{ + Exp: base64.RawURLEncoding.EncodeToString(big.NewInt(int64(p.E)).Bytes()), + N: base64.RawURLEncoding.EncodeToString(p.N.Bytes()), + } +} +*/ diff --git a/vendor/git.rootprojects.org/root/keypairs/jws.go b/vendor/git.rootprojects.org/root/keypairs/jws.go new file mode 100644 index 0000000..9d27c39 --- /dev/null +++ b/vendor/git.rootprojects.org/root/keypairs/jws.go @@ -0,0 +1,63 @@ +package keypairs + +import ( + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "strings" +) + +// JWS is a parsed JWT, representation as signable/verifiable and human-readable parts +type JWS struct { + Header Object `json:"header"` // JSON + Claims Object `json:"claims"` // JSON + Protected string `json:"protected"` // base64 + Payload string `json:"payload"` // base64 + Signature string `json:"signature"` // base64 +} + +// JWSToJWT joins JWS parts into a JWT as {ProtectedHeader}.{SerializedPayload}.{Signature}. +func JWSToJWT(jwt *JWS) string { + return fmt.Sprintf( + "%s.%s.%s", + jwt.Protected, + jwt.Payload, + jwt.Signature, + ) +} + +// JWTToJWS splits the JWT into its JWS segments +func JWTToJWS(jwt string) (jws *JWS) { + jwt = strings.TrimSpace(jwt) + parts := strings.Split(jwt, ".") + if 3 != len(parts) { + return nil + } + return &JWS{ + Protected: parts[0], + Payload: parts[1], + Signature: parts[2], + } +} + +// DecodeComponents decodes JWS Header and Claims +func (jws *JWS) DecodeComponents() error { + protected, err := base64.RawURLEncoding.DecodeString(jws.Protected) + if nil != err { + return errors.New("invalid JWS header base64Url encoding") + } + if err := json.Unmarshal([]byte(protected), &jws.Header); nil != err { + return errors.New("invalid JWS header") + } + + payload, err := base64.RawURLEncoding.DecodeString(jws.Payload) + if nil != err { + return errors.New("invalid JWS payload base64Url encoding") + } + if err := json.Unmarshal([]byte(payload), &jws.Claims); nil != err { + return errors.New("invalid JWS claims") + } + + return nil +} diff --git a/vendor/git.rootprojects.org/root/keypairs/marshal.go b/vendor/git.rootprojects.org/root/keypairs/marshal.go new file mode 100644 index 0000000..2198c5e --- /dev/null +++ b/vendor/git.rootprojects.org/root/keypairs/marshal.go @@ -0,0 +1,171 @@ +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, + )) +} diff --git a/vendor/git.rootprojects.org/root/keypairs/mock.go b/vendor/git.rootprojects.org/root/keypairs/mock.go new file mode 100644 index 0000000..2ca2a18 --- /dev/null +++ b/vendor/git.rootprojects.org/root/keypairs/mock.go @@ -0,0 +1,46 @@ +package keypairs + +import ( + "crypto/rsa" + "io" + "log" + mathrand "math/rand" +) + +// this shananigans is only for testing and debug API stuff +func (o *keyOptions) maybeMockReader() io.Reader { + if !allowMocking { + panic("mock method called when mocking is not allowed") + } + + if 0 == o.mockSeed { + return randReader + } + + log.Println("WARNING: MOCK: using insecure reader") + return mathrand.New(mathrand.NewSource(o.mockSeed)) +} + +const maxRetry = 16 + +func maybeDerandomizeMockKey(privkey PrivateKey, keylen int, opts *keyOptions) PrivateKey { + if 0 != opts.mockSeed { + for i := 0; i < maxRetry; i++ { + otherkey, _ := rsa.GenerateKey(opts.nextReader(), keylen) + otherCmp := otherkey.D.Cmp(privkey.(*rsa.PrivateKey).D) + if 0 != otherCmp { + // There are two possible keys, choose the lesser D value + // See https://github.com/square/go-jose/issues/189 + if otherCmp < 0 { + privkey = otherkey + } + break + } + if maxRetry == i-1 { + log.Printf("error: coinflip landed on heads %d times", maxRetry) + } + } + } + + return privkey +} diff --git a/vendor/git.rootprojects.org/root/keypairs/sign.go b/vendor/git.rootprojects.org/root/keypairs/sign.go new file mode 100644 index 0000000..59117ef --- /dev/null +++ b/vendor/git.rootprojects.org/root/keypairs/sign.go @@ -0,0 +1,165 @@ +package keypairs + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rsa" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + mathrand "math/rand" // to be used for good, not evil + "time" +) + +// Object is a type alias representing generic JSON data +type Object = map[string]interface{} + +// SignClaims adds `typ`, `kid` (or `jwk`), and `alg` in the header and expects claims for `jti`, `exp`, `iss`, and `iat` +func SignClaims(privkey PrivateKey, header Object, claims Object) (*JWS, error) { + var randsrc io.Reader = randReader + seed, _ := header["_seed"].(int64) + if 0 != seed { + randsrc = mathrand.New(mathrand.NewSource(seed)) + //delete(header, "_seed") + } + + protected, header, err := headerToProtected(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 +} diff --git a/vendor/git.rootprojects.org/root/keypairs/verify.go b/vendor/git.rootprojects.org/root/keypairs/verify.go new file mode 100644 index 0000000..f6dfae9 --- /dev/null +++ b/vendor/git.rootprojects.org/root/keypairs/verify.go @@ -0,0 +1,174 @@ +package keypairs + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rsa" + "crypto/sha256" + "crypto/subtle" + "encoding/base64" + "errors" + "fmt" + "log" + "math/big" + "time" +) + +// VerifyClaims will check the signature of a parsed JWT +func VerifyClaims(pubkey PublicKey, jws *JWS) (errs []error) { + kid, _ := jws.Header["kid"].(string) + jwkmap, hasJWK := jws.Header["jwk"].(Object) + //var jwk JWK = nil + + seed, _ := jws.Header["_seed"].(int64) + seedf64, _ := jws.Header["_seed"].(float64) + kty, _ := jws.Header["_kty"].(string) + if 0 == seed { + seed = int64(seedf64) + } + + var pub PublicKey = nil + if hasJWK { + pub, errs = selfsignCheck(jwkmap, errs) + } else { + opts := &keyOptions{mockSeed: seed, KeyType: kty} + pub, errs = pubkeyCheck(pubkey, kid, opts, errs) + } + + jti, _ := jws.Claims["jti"].(string) + expf64, _ := jws.Claims["exp"].(float64) + exp := int64(expf64) + if 0 == exp { + if "" == jti { + err := errors.New("one of 'jti' or 'exp' must exist for token expiry") + errs = append(errs, err) + } + } else { + if time.Now().Unix() > exp { + err := fmt.Errorf("token expired at %d (%s)", exp, time.Unix(exp, 0)) + errs = append(errs, err) + } + } + + signable := fmt.Sprintf("%s.%s", jws.Protected, jws.Payload) + hash := sha256.Sum256([]byte(signable)) + sig, err := base64.RawURLEncoding.DecodeString(jws.Signature) + if nil != err { + err := fmt.Errorf("could not decode signature: %w", err) + errs = append(errs, err) + return errs + } + + //log.Printf("\n(Verify)\nSignable: %s", signable) + //log.Printf("Hash: %s", hash) + //log.Printf("Sig: %s", jws.Signature) + if nil == pub { + err := fmt.Errorf("token signature could not be verified") + errs = append(errs, err) + } else if !Verify(pub, hash[:], sig) { + err := fmt.Errorf("token signature is not valid") + errs = append(errs, err) + } + return errs +} + +func selfsignCheck(jwkmap Object, errs []error) (PublicKey, []error) { + var pub PublicKey = nil + log.Println("Security TODO: did not check jws.Claims[\"sub\"] against 'jwk'") + log.Println("Security TODO: did not check jws.Claims[\"iss\"]") + kty := jwkmap["kty"] + var err error + if "RSA" == kty { + e, _ := jwkmap["e"].(string) + n, _ := jwkmap["n"].(string) + k, _ := (&RSAJWK{ + Exp: e, + N: n, + }).marshalJWK() + pub, err = ParseJWKPublicKey(k) + if nil != err { + return nil, append(errs, err) + } + } else { + crv, _ := jwkmap["crv"].(string) + x, _ := jwkmap["x"].(string) + y, _ := jwkmap["y"].(string) + k, _ := (&ECJWK{ + Curve: crv, + X: x, + Y: y, + }).marshalJWK() + pub, err = ParseJWKPublicKey(k) + if nil != err { + return nil, append(errs, err) + } + } + + return pub, errs +} + +func pubkeyCheck(pubkey PublicKey, kid string, opts *keyOptions, errs []error) (PublicKey, []error) { + var pub PublicKey = nil + + if "" == kid { + err := errors.New("token should have 'kid' or 'jwk' in header to identify the public key") + errs = append(errs, err) + } + + if nil == pubkey { + if allowMocking { + if 0 == opts.mockSeed { + err := errors.New("the debug API requires '_seed' to accompany 'kid'") + errs = append(errs, err) + } + if "" == opts.KeyType { + err := errors.New("the debug API requires '_kty' to accompany '_seed'") + errs = append(errs, err) + } + + if 0 == opts.mockSeed || "" == opts.KeyType { + return nil, errs + } + privkey := newPrivateKey(opts) + pub = NewPublicKey(privkey.Public()) + return pub, errs + } + err := errors.New("no matching public key") + errs = append(errs, err) + } else { + pub = pubkey + } + + if nil != pub && "" != kid { + if 1 != subtle.ConstantTimeCompare([]byte(kid), []byte(pub.Thumbprint())) { + err := errors.New("'kid' does not match the public key thumbprint") + errs = append(errs, err) + } + } + return pub, errs +} + +// Verify will check the signature of a hash +func Verify(pubkey PublicKey, hash []byte, sig []byte) bool { + + switch pub := pubkey.Key().(type) { + case *rsa.PublicKey: + //log.Printf("RSA VERIFY") + // TODO Size(key) to detect key size ? + //alg := "SHA256" + // TODO: this hasn't been tested yet + if err := rsa.VerifyPKCS1v15(pub, crypto.SHA256, hash, sig); nil != err { + return false + } + return true + case *ecdsa.PublicKey: + r := &big.Int{} + r.SetBytes(sig[0:32]) + s := &big.Int{} + s.SetBytes(sig[32:]) + return ecdsa.Verify(pub, hash, r, s) + default: + panic("impossible condition: non-rsa/non-ecdsa key") + //return false + } +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 52bd083..43f582d 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1,6 +1,6 @@ # git.rootprojects.org/root/hashcash v1.0.1 git.rootprojects.org/root/hashcash -# git.rootprojects.org/root/keypairs v0.5.2 +# git.rootprojects.org/root/keypairs v0.6.5 git.rootprojects.org/root/keypairs git.rootprojects.org/root/keypairs/keyfetch git.rootprojects.org/root/keypairs/keyfetch/uncached