Skip to content

Commit 1a1ea73

Browse files
authored
[jwk] Add the ability to work with private fields in jwk.Set (#502)
* [jwk] Add the ability to work with private fields in jwk.Set * appease linter * check case when JWKS == JWK
1 parent 74b6426 commit 1a1ea73

5 files changed

Lines changed: 233 additions & 28 deletions

File tree

Changes

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
Changes
22
=======
33

4+
v1.2.12 UNRELEASED
5+
[New Features]
6+
* `jwk.Set` can now parse private parameters. For example, after parsing
7+
`{"foo": "bar", "keys": [...]}`, users can get to the value of `"foo"`
8+
by calling `set.Field("foo")`
9+
410
v1.2.11 14 Nov 2021
511
[Security Fix]
612
* It was reported that since v1.2.6, it was possible to craft

jwk/interface.go

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@ const (
4848
// `"encoding/json".Marshal` and `"encoding/json".Unmarshal`. However,
4949
// if you do not know if the payload contains a single JWK or a JWK set,
5050
// consider using `jwk.Parse()` to always get a `jwk.Set` out of it.
51+
//
52+
// Since v1.2.12, JWK sets with private parameters can be parsed as well.
53+
// Such private parameters can be accessed via the `Field()` method.
54+
// If a resource contains a single JWK instead of a JWK set, private parameters
55+
// are stored in _both_ the resulting `jwk.Set` object and the `jwk.Key` object .
56+
//
5157
type Set interface {
5258
// Add adds the specified key. If the key already exists in the set, it is
5359
// not added.
@@ -59,8 +65,15 @@ type Set interface {
5965

6066
// Get returns the key at index `idx`. If the index is out of range,
6167
// then the second return value is false.
68+
// This method will be renamed to `Key(int)` in a future major release.
6269
Get(int) (Key, bool)
6370

71+
// Field returns the value of a private field in the key set.
72+
// For the purposes of a key set, any field other than the "keys" field is
73+
// considered to be a private field.
74+
// This method will be renamed to `Get(string)` in a future major release.
75+
Field(string) (interface{}, bool)
76+
6477
// Index returns the index where the given key exists, -1 otherwise
6578
Index(Key) int
6679

@@ -84,9 +97,10 @@ type Set interface {
8497
}
8598

8699
type set struct {
87-
keys []Key
88-
mu sync.RWMutex
89-
dc DecodeCtx
100+
keys []Key
101+
mu sync.RWMutex
102+
dc DecodeCtx
103+
privateParams map[string]interface{}
90104
}
91105

92106
type HeaderVisitor = iter.MapVisitor

jwk/jwk.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -466,7 +466,7 @@ func ParseKey(data []byte, options ...ParseOption) (Key, error) {
466466
// call `json.Unmarshal` against an empty set created by `jwk.NewSet()`
467467
// to parse a JSON buffer into a `jwk.Set`.
468468
//
469-
// This method exists because many times the user does not know before hand
469+
// This function exists because many times the user does not know before hand
470470
// if a JWK(s) resource at a remote location contains a single JWK key or
471471
// a JWK set, and `jwk.Parse()` can handle either case, returning a JWK Set
472472
// even if the data only contains a single JWK key

jwk/jwk_test.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package jwk_test
22

33
import (
4+
"bytes"
45
"context"
56
"crypto"
67
"crypto/ecdsa"
@@ -1653,3 +1654,119 @@ func TestGH491(t *testing.T) {
16531654
return
16541655
}
16551656
}
1657+
1658+
func TestSetWithPrivateParams(t *testing.T) {
1659+
k1, err := jwxtest.GenerateRsaJwk()
1660+
if !assert.NoError(t, err, `jwxtest.GenerateRsaJwk should succeed`) {
1661+
return
1662+
}
1663+
k2, err := jwxtest.GenerateEcdsaJwk()
1664+
if !assert.NoError(t, err, `jwxtest.GenerateEcdsaJwk should succeed`) {
1665+
return
1666+
}
1667+
k3, err := jwxtest.GenerateSymmetricJwk()
1668+
if !assert.NoError(t, err, `jwxtest.GenerateSymmetricJwk should succeed`) {
1669+
return
1670+
}
1671+
1672+
t.Run("JWK instead of JWKS", func(t *testing.T) {
1673+
var buf bytes.Buffer
1674+
_ = k1.Set(`renewal_kid`, "foo")
1675+
_ = json.NewEncoder(&buf).Encode(k1)
1676+
1677+
var check = func(t *testing.T, buf []byte) {
1678+
set, err := jwk.Parse(buf)
1679+
if !assert.NoError(t, err, `jwk.Parse should succeed`) {
1680+
return
1681+
}
1682+
1683+
if !assert.Equal(t, 1, set.Len(), `set.Len() should be 1`) {
1684+
return
1685+
}
1686+
1687+
v, ok := set.Field(`renewal_kid`)
1688+
if !assert.True(t, ok, `set.Field("renewal_kid") should return ok = true`) {
1689+
return
1690+
}
1691+
1692+
if !assert.Equal(t, `foo`, v, `set.Field("renewal_kid") should return "foo"`) {
1693+
return
1694+
}
1695+
1696+
key, ok := set.Get(0)
1697+
if !assert.True(t, ok, `set.Get(0) should return ok = true`) {
1698+
return
1699+
}
1700+
1701+
v, ok = key.Get(`renewal_kid`)
1702+
if !assert.True(t, ok, `key.Get("renewal_kid") should return ok = true`) {
1703+
return
1704+
}
1705+
1706+
if !assert.Equal(t, `foo`, v, `key.Get("renewal_kid") should return "foo"`) {
1707+
return
1708+
}
1709+
}
1710+
1711+
t.Run("Check original buffer", func(t *testing.T) {
1712+
check(t, buf.Bytes())
1713+
})
1714+
t.Run("Check serialized", func(t *testing.T) {
1715+
set, err := jwk.Parse(buf.Bytes())
1716+
if !assert.NoError(t, err, `jwk.Parse should succeed`) {
1717+
return
1718+
}
1719+
js, err := json.MarshalIndent(set, "", " ")
1720+
if !assert.NoError(t, err, `json.MarshalIndent should succeed`) {
1721+
return
1722+
}
1723+
check(t, js)
1724+
})
1725+
})
1726+
t.Run("JWKS with multiple keys", func(t *testing.T) {
1727+
var buf bytes.Buffer
1728+
buf.WriteString(`{"renewal_kid":"foo","keys":[`)
1729+
enc := json.NewEncoder(&buf)
1730+
_ = enc.Encode(k1)
1731+
buf.WriteByte(',')
1732+
_ = enc.Encode(k2)
1733+
buf.WriteByte(',')
1734+
_ = enc.Encode(k3)
1735+
buf.WriteString(`]}`)
1736+
1737+
var check = func(t *testing.T, buf []byte) {
1738+
set, err := jwk.Parse(buf)
1739+
if !assert.NoError(t, err, `jwk.Parse should succeed`) {
1740+
return
1741+
}
1742+
1743+
if !assert.Equal(t, 3, set.Len(), `set.Len() should be 3`) {
1744+
return
1745+
}
1746+
1747+
v, ok := set.Field(`renewal_kid`)
1748+
if !assert.True(t, ok, `set.Field("renewal_kid") should return ok = true`) {
1749+
return
1750+
}
1751+
1752+
if !assert.Equal(t, `foo`, v, `set.Field("renewal_kid") should return "foo"`) {
1753+
return
1754+
}
1755+
}
1756+
1757+
t.Run("Check original buffer", func(t *testing.T) {
1758+
check(t, buf.Bytes())
1759+
})
1760+
t.Run("Check serialized", func(t *testing.T) {
1761+
set, err := jwk.Parse(buf.Bytes())
1762+
if !assert.NoError(t, err, `jwk.Parse should succeed`) {
1763+
return
1764+
}
1765+
js, err := json.MarshalIndent(set, "", " ")
1766+
if !assert.NoError(t, err, `json.MarshalIndent should succeed`) {
1767+
return
1768+
}
1769+
check(t, js)
1770+
})
1771+
})
1772+
}

jwk/set.go

Lines changed: 92 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package jwk
22

33
import (
4+
"bytes"
45
"context"
6+
"fmt"
7+
"sort"
58

69
"github.com/lestrrat-go/iter/arrayiter"
710
"github.com/lestrrat-go/jwx/internal/json"
@@ -11,7 +14,17 @@ import (
1114

1215
// NewSet creates and empty `jwk.Set` object
1316
func NewSet() Set {
14-
return &set{}
17+
return &set{
18+
privateParams: make(map[string]interface{}),
19+
}
20+
}
21+
22+
func (s *set) Field(n string) (interface{}, bool) {
23+
s.mu.RLock()
24+
defer s.mu.RUnlock()
25+
26+
v, ok := s.privateParams[n]
27+
return v, ok
1528
}
1629

1730
func (s *set) Get(idx int) (Key, bool) {
@@ -105,10 +118,6 @@ func iterate(ctx context.Context, keys []Key, ch chan *KeyPair) {
105118
}
106119
}
107120

108-
type keySetMarshalProxy struct {
109-
Keys []json.RawMessage `json:"keys"`
110-
}
111-
112121
func (s *set) MarshalJSON() ([]byte, error) {
113122
s.mu.RLock()
114123
defer s.mu.RUnlock()
@@ -117,16 +126,36 @@ func (s *set) MarshalJSON() ([]byte, error) {
117126
defer pool.ReleaseBytesBuffer(buf)
118127
enc := json.NewEncoder(buf)
119128

120-
buf.WriteString(`{"keys":[`)
121-
for i, k := range s.keys {
129+
fields := []string{`keys`}
130+
for k := range s.privateParams {
131+
fields = append(fields, k)
132+
}
133+
sort.Strings(fields)
134+
135+
buf.WriteByte('{')
136+
for i, field := range fields {
122137
if i > 0 {
123138
buf.WriteByte(',')
124139
}
125-
if err := enc.Encode(k); err != nil {
126-
return nil, errors.Wrapf(err, `failed to marshal key #%d`, i)
140+
fmt.Fprintf(buf, `%q:`, field)
141+
if field != `keys` {
142+
if err := enc.Encode(s.privateParams[field]); err != nil {
143+
return nil, errors.Wrapf(err, `failed to marshal field %q`, field)
144+
}
145+
} else {
146+
buf.WriteByte('[')
147+
for j, k := range s.keys {
148+
if j > 0 {
149+
buf.WriteByte(',')
150+
}
151+
if err := enc.Encode(k); err != nil {
152+
return nil, errors.Wrapf(err, `failed to marshal key #%d`, i)
153+
}
154+
}
155+
buf.WriteByte(']')
127156
}
128157
}
129-
buf.WriteString("]}")
158+
buf.WriteByte('}')
130159

131160
ret := make([]byte, buf.Len())
132161
copy(ret, buf.Bytes())
@@ -137,10 +166,8 @@ func (s *set) UnmarshalJSON(data []byte) error {
137166
s.mu.Lock()
138167
defer s.mu.Unlock()
139168

140-
var proxy keySetMarshalProxy
141-
if err := json.Unmarshal(data, &proxy); err != nil {
142-
return errors.Wrap(err, `failed to unmarshal into Key (proxy)`)
143-
}
169+
s.privateParams = make(map[string]interface{})
170+
s.keys = nil
144171

145172
var options []ParseOption
146173
if dc := s.dc; dc != nil {
@@ -149,20 +176,61 @@ func (s *set) UnmarshalJSON(data []byte) error {
149176
}
150177
}
151178

152-
if len(proxy.Keys) == 0 {
153-
k, err := ParseKey(data, options...)
179+
var sawKeysField bool
180+
dec := json.NewDecoder(bytes.NewReader(data))
181+
LOOP:
182+
for {
183+
tok, err := dec.Token()
154184
if err != nil {
155-
return errors.Wrap(err, `failed to unmarshal key from JSON headers`)
185+
return errors.Wrap(err, `error reading token`)
156186
}
157-
s.keys = append(s.keys, k)
158-
} else {
159-
for i, buf := range proxy.Keys {
160-
k, err := ParseKey([]byte(buf), options...)
161-
if err != nil {
162-
return errors.Wrapf(err, `failed to unmarshal key #%d (total %d) from multi-key JWK set`, i+1, len(proxy.Keys))
187+
188+
switch tok := tok.(type) {
189+
case json.Delim:
190+
// Assuming we're doing everything correctly, we should ONLY
191+
// get either '{' or '}' here.
192+
if tok == '}' { // End of object
193+
break LOOP
194+
} else if tok != '{' {
195+
return errors.Errorf(`expected '{', but got '%c'`, tok)
163196
}
164-
s.keys = append(s.keys, k)
197+
case string:
198+
switch tok {
199+
case "keys":
200+
sawKeysField = true
201+
var list []json.RawMessage
202+
if err := dec.Decode(&list); err != nil {
203+
return errors.Wrap(err, `failed to decode "keys"`)
204+
}
205+
206+
for i, keysrc := range list {
207+
key, err := ParseKey(keysrc, options...)
208+
if err != nil {
209+
return errors.Wrapf(err, `failed to code key #%d in "keys"`, i)
210+
}
211+
s.keys = append(s.keys, key)
212+
}
213+
default:
214+
var v interface{}
215+
if err := dec.Decode(&v); err != nil {
216+
return errors.Wrapf(err, `failed to decode value for key %q`, tok)
217+
}
218+
s.privateParams[tok] = v
219+
}
220+
}
221+
}
222+
223+
// This is really silly, but we can only detect the
224+
// lack of the "keys" field after going through the
225+
// entire object once
226+
// Not checking for len(s.keys) == 0, because it could be
227+
// an empty key set
228+
if !sawKeysField {
229+
key, err := ParseKey(data, options...)
230+
if err != nil {
231+
return errors.Wrapf(err, `failed to parse sole key in key set`)
165232
}
233+
s.keys = append(s.keys, key)
166234
}
167235
return nil
168236
}

0 commit comments

Comments
 (0)