Skip to content

Commit

Permalink
Introduce jwk.CachedSet to make the common operation of using cached …
Browse files Browse the repository at this point in the history
…JWKS easier (lestrrat-go#689)

* First round implementing jwk.CachedSet

* Change jwk.Set interface

* appease linter

* add docs

* Fix example code

* fix jwk.Set usage

* Add codecov.yml

* Add more tests

* appease linter

* tweak docs, examples, go versions

* Update deps for go1.18
  • Loading branch information
lestrrat authored Apr 16, 2022
1 parent 6de427f commit 2c8c48e
Show file tree
Hide file tree
Showing 37 changed files with 450 additions and 171 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
strategy:
matrix:
go_tags: [ 'stdlib', 'goccy', 'es256k', 'all']
go: [ '1.17.x', '1.16.x' ]
go: [ '1.18.x', '1.17.x' ]
name: "Test [ Go ${{ matrix.go }} / Tags ${{ matrix.go_tags }} ]"
steps:
- name: Checkout repository
Expand Down
22 changes: 22 additions & 0 deletions Changes-v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,28 @@ jwe.Verify(signed, jwe.WithKeySet(jwks), jwe.WithKeyUsed(&keyUsed))
* `jwk.New()` has been renamed to `jwk.FromRaw()`, which hopefully will
make it easier for the users what the input should be.

* `jwk.Set` has many interface changes:
* Changed methods to match jwk.Key and its semantics:
* Field is now Get() (returns values for arbitrary fields other than keys). Fetching a key is done via Key()
* Remove() now removes arbitrary fields, not keys. to remove keys, use RemoveKey()
* Iterate has been added to iterate through all non-key fields.
* Add is now AddKey(Key) string
* Get is now Key(int) (Key, bool)
* Remove is now RemoveKey(Key) error
* Iterate is now Keys(context.Context) KeyIterator
* Clear is now Clear() error

* `jwk.CachedSet` has been added. You can create a `jwk.Set` that is backed by
`jwk.Cache` so you can do this:

```go
cache := jkw.NewCache(ctx)
cachedSet := jwk.NewCachedSet(cache, jwksURI)

// cachedSet is always the refreshed, cached version from jwk.Cache
jws.Verify(signed, jws.WithKeySet(cachedSet))
```

* `jwk.NewRSAPRivateKey()`, `jwk.NewECDSAPrivateKey()`, etc have been removed.
There is no longer any way to create concrete types of `jwk.Key`

Expand Down
6 changes: 0 additions & 6 deletions bench/comparison/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,3 @@ module github.com/lestrrat-go/jwx/v2/bench/comparison
go 1.15

replace github.com/lestrrat-go/jwx/v2 => ../..

require (
github.com/golang-jwt/jwt/v4 v4.4.0
github.com/lestrrat-go/jwx/v2 v2.0.0-00010101000000-000000000000
github.com/stretchr/testify v1.7.1
)
38 changes: 0 additions & 38 deletions bench/comparison/go.sum
Original file line number Diff line number Diff line change
@@ -1,38 +0,0 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
github.com/goccy/go-json v0.9.6 h1:5/4CtRQdtsX0sal8fdVhTaiMN01Ri8BExZZ8iRmHQ6E=
github.com/goccy/go-json v0.9.6/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang-jwt/jwt/v4 v4.4.0 h1:EmVIxB5jzbllGIjiCV5JG4VylbK3KE400tLGLI1cdfU=
github.com/golang-jwt/jwt/v4 v4.4.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80=
github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
github.com/lestrrat-go/httprc v1.0.1 h1:Cnc4NxIySph38pQPzKbjg5OkKsGR/Cf5xcWt5OlSUDI=
github.com/lestrrat-go/httprc v1.0.1/go.mod h1:5Ml+nB++j6IC0e6LzefJnrpMQDKgDwDCaIQQzhbqhJM=
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4=
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292 h1:f+lwQ+GtmgoY+A2YaQxlSOnDjXcQ7ZRLWOHbC6HtRqE=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
4 changes: 2 additions & 2 deletions cmd/jwx/jwe.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ func makeJweEncryptCmd() *cli.Command {
if keyset.Len() != 1 {
return fmt.Errorf(`jwk file must contain exactly one key`)
}
key, _ := keyset.Get(0)
key, _ := keyset.Key(0)

pubkey, err := jwk.PublicKeyOf(key)
if err != nil {
Expand Down Expand Up @@ -151,7 +151,7 @@ func makeJweDecryptCmd() *cli.Command {
if keyset.Len() != 1 {
return fmt.Errorf(`jwk file must contain exactly one key`)
}
key, _ := keyset.Get(0)
key, _ := keyset.Key(0)

var decrypted []byte

Expand Down
4 changes: 2 additions & 2 deletions cmd/jwx/jwk.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ func dumpJWKSet(dst io.Writer, keyset jwk.Set, format string, preserve bool) err
return fmt.Errorf(`failed to marshal keyset into JSON format: %w`, err)
}
} else {
key, _ := keyset.Get(0)
key, _ := keyset.Key(0)
if err := dumpJSON(dst, key); err != nil {
return fmt.Errorf(`failed to marshal key into JSON format: %w`, err)
}
Expand Down Expand Up @@ -203,7 +203,7 @@ func makeJwkGenerateCmd() *cli.Command {
}

keyset := jwk.NewSet()
keyset.Add(key)
keyset.AddKey(key)

if c.Bool("public-key") {
pubks, err := jwk.PublicSetOf(keyset)
Expand Down
4 changes: 2 additions & 2 deletions cmd/jwx/jws.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ func makeJwsVerifyCmd() *cli.Command {
}

ctx := context.Background()
for iter := keyset.Iterate(ctx); iter.Next(ctx); {
for iter := keyset.Keys(ctx); iter.Next(ctx); {
pair := iter.Pair()
key := pair.Value.(jwk.Key)
payload, err := jws.Verify(buf, jws.WithKey(alg, key))
Expand Down Expand Up @@ -232,7 +232,7 @@ func makeJwsSignCmd() *cli.Command {
if keyset.Len() != 1 {
return fmt.Errorf(`jwk file must contain exactly one key`)
}
key, _ := keyset.Get(0)
key, _ := keyset.Key(0)

src, err := getSource(c.Args().Get(0))
if err != nil {
Expand Down
2 changes: 2 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
codecov:
allow_coverage_offsets: true
7 changes: 5 additions & 2 deletions docs/04-jwk.md
Original file line number Diff line number Diff line change
Expand Up @@ -524,7 +524,7 @@ source: [examples/jwk_from_raw_example_test.go](https://github.com/lestrrat-go/j

To parse keys stored in a remote location pointed by a HTTP(s) URL, use [`jwk.Fetch()`](https://pkg.go.dev/github.com/lestrrat-go/jwx/v2/jwk#Fetch)

If you are going to be using this key repeatedly in a long running process, consider using [`jwk.Cache`](https://pkg.go.dev/github.com/lestrrat-go/jwx/v2/jwk#Cache) described elsewhere in this document.
If you are going to be using this key repeatedly in a long running process, consider using [`jwk.Cache`](https://pkg.go.dev/github.com/lestrrat-go/jwx/v2/jwk#Cache) or [`jwk.CachedSet`](https://pkg.go.dev/github.com/lestrrat-go/jwx/v2/jwk#CachedSet) described elsewhere in this document.

<!-- INCLUDE(examples/jwk_fetch_example_test.go) -->
```go
Expand Down Expand Up @@ -590,7 +590,7 @@ Normally, you should be able to simply fetch the JWK using [`jwk.Fetch()`](https
but keys are usually routinely expired and rotated due to security reasons.
In such cases you would need to refetch the JWK periodically, which is a pain.

`github.com/lestrrat-go/jwx/v2/jwk` provides the [`jwk.Cache`](https://pkg.go.dev/github.com/lestrrat-go/jwx/v2/jwk#Cache) tool to do this for you.
`github.com/lestrrat-go/jwx/v2/jwk` provides the [`jwk.Cache`](https://pkg.go.dev/github.com/lestrrat-go/jwx/v2/jwk#Cache) and [`jwk.CachedSet`](https://pkg.go.dev/github.com/lestrrat-go/jwx/v2/jwk#CachedSet) to do this for you.

<!-- INCLUDE(examples/jwk_cache_example_test.go) -->
```go
Expand Down Expand Up @@ -669,6 +669,9 @@ MAIN:
source: [examples/jwk_cache_example_test.go](https://github.com/lestrrat-go/jwx/blob/v2/examples/jwk_cache_example_test.go)
<!-- END INCLUDE -->

<!-- INCLUDE(examples/jwk_cached_set_example_test.go) -->
<!-- END INCLUDE -->

## Using Whitelists

If you are fetching JWK Sets from a possibly untrusted source such as the URL in the`"jku"` field of a JWS message,
Expand Down
6 changes: 3 additions & 3 deletions examples/jwe_decrypt_with_keyset_example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@ func ExampleJWE_VerifyWithJWKSet() {
set := jwk.NewSet()
// Add some bogus keys
k1, _ := jwk.FromRaw([]byte("abracadavra"))
set.Add(k1)
set.AddKey(k1)
k2, _ := jwk.FromRaw([]byte("opensasame"))
set.Add(k2)
set.AddKey(k2)
// Add the real thing
k3, _ := jwk.FromRaw(privkey)
k3.Set(jwk.AlgorithmKey, jwa.RSA_OAEP)
set.Add(k3)
set.AddKey(k3)

// Up to this point, you probably will replace with a simple jwk.Fetch()

Expand Down
40 changes: 40 additions & 0 deletions examples/jwk_cached_set_example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package examples_test

import (
"context"
"fmt"
"time"

"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/lestrrat-go/jwx/v2/jws"
)

func ExampleJWK_CachedSet() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

const googleCerts = `https://www.googleapis.com/oauth2/v3/certs`

// The first steps are the same as examples/jwk_cache_example_test.go
c := jwk.NewCache(ctx)
c.Register(googleCerts, jwk.WithMinRefreshInterval(15*time.Minute))
_, err := c.Refresh(ctx, googleCerts)
if err != nil {
fmt.Printf("failed to refresh google JWKS: %s\n", err)
return
}

cached := jwk.NewCachedSet(c, googleCerts)

// cached fulfills the jwk.Set interface.
var _ jwk.Set = cached

// That means you can pass it to things like jws.WithKeySet,
// allowing you to pretend as if you are using the result of
//
// jwk.Fetch(ctx, googleCerts)
//
// But you are instead using a cached (and periodically refreshed)
// for each operation.
_ = jws.WithKeySet(cached)
}
8 changes: 4 additions & 4 deletions examples/jws_verify_with_keyset_example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,14 @@ func ExampleJWS_VerifyWithJWKSet() {
set := jwk.NewSet()
// Add some bogus keys
k1, _ := jwk.FromRaw([]byte("abracadavra"))
set.Add(k1)
set.AddKey(k1)
k2, _ := jwk.FromRaw([]byte("opensasame"))
set.Add(k2)
// Add the real thing
set.AddKey(k2)
// AddKey the real thing
pubkey, _ := jwk.PublicRawKeyOf(privkey)
k3, _ := jwk.FromRaw(pubkey)
k3.Set(jwk.AlgorithmKey, jwa.RS256)
set.Add(k3)
set.AddKey(k3)

// Up to this point, you probably will replace with a simple jwk.Fetch()

Expand Down
2 changes: 1 addition & 1 deletion examples/jwt_example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func ExampleJWT_ParseWithJWKS() {

// This JWKS can *only* have 1 key.
keyset = jwk.NewSet()
keyset.Add(pubKey)
keyset.AddKey(pubKey)
}

{
Expand Down
2 changes: 1 addition & 1 deletion examples/jwt_parse_with_jku_example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func ExampleJWT_ParseWithJKU() {
fmt.Printf("failed to create public key: %s\n", err)
return
}
set.Add(pubkey)
set.AddKey(pubkey)
}

srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expand Down
4 changes: 2 additions & 2 deletions examples/jwt_parse_with_keyset_example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ func ExampleJWT_ParseWithKeySet() {
// all of the public keys
{
privset := jwk.NewSet()
privset.Add(realKey)
privset.Add(bogusKey)
privset.AddKey(realKey)
privset.AddKey(bogusKey)
v, err := jwk.PublicSetOf(privset)
if err != nil {
fmt.Printf("failed to create public JWKS: %s\n", err)
Expand Down
2 changes: 1 addition & 1 deletion jwe/jwe_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ func TestParse_RSAES_OAEP_AES_GCM(t *testing.T) {
// Keys are not going to be selected without an algorithm
_ = pkJwk.Set(jwe.AlgorithmKey, jwa.RSA_OAEP)
set := jwk.NewSet()
set.Add(pkJwk)
set.AddKey(pkJwk)

var used interface{}
plaintext, err = jwe.Decrypt(encrypted, jwe.WithKeySet(set, jwe.WithRequireKid(false)), jwe.WithKeyUsed(&used))
Expand Down
2 changes: 1 addition & 1 deletion jwe/key_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ func (kp *keySetProvider) FetchKeys(_ context.Context, sink KeySink, r Recipient
}

for i := 0; i < kp.set.Len(); i++ {
key, _ := kp.set.Get(i)
key, _ := kp.set.Key(i)
if err := kp.selectKey(sink, key, r, msg); err != nil {
continue
}
Expand Down
Loading

0 comments on commit 2c8c48e

Please sign in to comment.