Skip to content

Commit 7f8e202

Browse files
authored
fix inconsistent mutex locking in data structures (#1666)
* fix inconsistent mutex locking in data structures * suppress forcetypeassert on rlocker assertions * fix Thumbprint value receivers copying mutex
1 parent 6e4eb95 commit 7f8e202

18 files changed

Lines changed: 499 additions & 181 deletions

File tree

Changes

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ v3 has many incompatibilities with v2. To see the full list of differences betwe
55
v2 and v3, please read the Changes-v3.md file (https://github.com/lestrrat-go/jwx/blob/develop/v3/Changes-v3.md)
66

77
v3.0.14 UNRELEASED
8+
* [jwt][jwe][jws][jwk] Fix inconsistent mutex locking across main data
9+
structures. Named getters on JWK key types, MarshalJSON on JWK keys,
10+
UnmarshalJSON on JWE headers, makePairs/MarshalJSON on JWT tokens,
11+
rawBuffer on JWS headers, and Set/Keys on jwk.Set were missing proper
12+
lock protection. Switch all mutex fields from *sync.RWMutex (pointer)
13+
to sync.RWMutex (value) so go vet -copylocks catches accidental copies,
14+
and convert affected value-receiver methods to pointer receivers.
15+
816
* [jwt][jwe][jws] Add `WithMaxParseInputSize(int64)` to limit bytes read from
917
an `io.Reader` in `ParseReader` and `ReadFile`. Default is 10 MB. Can be set
1018
globally via `Settings()` or per-call. (#1630, #1632)

jwe/headers_gen.go

Lines changed: 4 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

jwk/ecdsa.go

Lines changed: 75 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -170,47 +170,72 @@ func ecdsaJWKToRaw(keyif Key, hint any) (any, error) {
170170
}
171171
}
172172

173-
locker, ok := k.(rlocker)
174-
if ok {
173+
// rlocker is unexported with unexported methods, so only our
174+
// concrete types implement it. A successful assertion lets us
175+
// type-assert to the concrete struct and read fields directly
176+
// under a single batch lock. This avoids nested RLock (which
177+
// deadlocks when a writer is pending) while preserving an
178+
// atomic snapshot of all fields.
179+
var crv jwa.EllipticCurveAlgorithm
180+
var hasCrv bool
181+
var od, ox, oy []byte
182+
if locker, ok := k.(rlocker); ok {
175183
locker.rlock()
176-
defer locker.runlock()
184+
concrete := k.(*ecdsaPrivateKey) //nolint:forcetypeassert // rlocker is unexported; only our concrete types implement it
185+
if concrete.crv != nil {
186+
crv = *(concrete.crv)
187+
hasCrv = true
188+
}
189+
od, ox, oy = concrete.d, concrete.x, concrete.y
190+
locker.runlock()
191+
} else {
192+
// External implementation — use self-locking interface getters.
193+
var ok bool
194+
if crv, ok = k.Crv(); !ok {
195+
return nil, fmt.Errorf(`missing "crv" field`)
196+
}
197+
hasCrv = true
198+
if od, ok = k.D(); !ok {
199+
return nil, fmt.Errorf(`missing "d" field`)
200+
}
201+
if ox, ok = k.X(); !ok {
202+
return nil, fmt.Errorf(`missing "x" field`)
203+
}
204+
if oy, ok = k.Y(); !ok {
205+
return nil, fmt.Errorf(`missing "y" field`)
206+
}
177207
}
178208

179-
crv, ok := k.Crv()
180-
if !ok {
209+
if !hasCrv {
181210
return nil, fmt.Errorf(`missing "crv" field`)
182211
}
183212

184213
if isECDH {
185-
d, ok := k.D()
186-
if !ok {
214+
if od == nil {
187215
return nil, fmt.Errorf(`missing "d" field`)
188216
}
189-
return buildECDHPrivateKey(crv, d)
217+
return buildECDHPrivateKey(crv, od)
190218
}
191219

192-
x, ok := k.X()
193-
if !ok {
220+
if ox == nil {
194221
return nil, fmt.Errorf(`missing "x" field`)
195222
}
196-
y, ok := k.Y()
197-
if !ok {
223+
if oy == nil {
198224
return nil, fmt.Errorf(`missing "y" field`)
199225
}
200-
pubk, err := buildECDSAPublicKey(crv, x, y)
226+
if od == nil {
227+
return nil, fmt.Errorf(`missing "d" field`)
228+
}
229+
230+
pubk, err := buildECDSAPublicKey(crv, ox, oy)
201231
if err != nil {
202232
return nil, fmt.Errorf(`failed to build public key: %w`, err)
203233
}
204234

205235
var key ecdsa.PrivateKey
206236
var d big.Int
207237

208-
origD, ok := k.D()
209-
if !ok {
210-
return nil, fmt.Errorf(`missing "d" field`)
211-
}
212-
213-
d.SetBytes(origD)
238+
d.SetBytes(od)
214239
key.D = &d
215240
key.PublicKey = *pubk
216241

@@ -231,24 +256,40 @@ func ecdsaJWKToRaw(keyif Key, hint any) (any, error) {
231256
}
232257
}
233258

234-
locker, ok := k.(rlocker)
235-
if ok {
259+
// See ECDSAPrivateKey case above for explanation of the rlocker pattern.
260+
var crv jwa.EllipticCurveAlgorithm
261+
var hasCrv bool
262+
var x, y []byte
263+
if locker, ok := k.(rlocker); ok {
236264
locker.rlock()
237-
defer locker.runlock()
265+
concrete := k.(*ecdsaPublicKey) //nolint:forcetypeassert // rlocker is unexported; only our concrete types implement it
266+
if concrete.crv != nil {
267+
crv = *(concrete.crv)
268+
hasCrv = true
269+
}
270+
x, y = concrete.x, concrete.y
271+
locker.runlock()
272+
} else {
273+
var ok bool
274+
if crv, ok = k.Crv(); !ok {
275+
return nil, fmt.Errorf(`missing "crv" field`)
276+
}
277+
hasCrv = true
278+
if x, ok = k.X(); !ok {
279+
return nil, fmt.Errorf(`missing "x" field`)
280+
}
281+
if y, ok = k.Y(); !ok {
282+
return nil, fmt.Errorf(`missing "y" field`)
283+
}
238284
}
239285

240-
crv, ok := k.Crv()
241-
if !ok {
286+
if !hasCrv {
242287
return nil, fmt.Errorf(`missing "crv" field`)
243288
}
244-
245-
x, ok := k.X()
246-
if !ok {
289+
if x == nil {
247290
return nil, fmt.Errorf(`missing "x" field`)
248291
}
249-
250-
y, ok := k.Y()
251-
if !ok {
292+
if y == nil {
252293
return nil, fmt.Errorf(`missing "y" field`)
253294
}
254295
if isECDH {
@@ -305,12 +346,12 @@ func ecdsaThumbprint(hash crypto.Hash, crv, x, y string) []byte {
305346

306347
// Thumbprint returns the JWK thumbprint using the indicated
307348
// hashing algorithm, according to RFC 7638
308-
func (k ecdsaPublicKey) Thumbprint(hash crypto.Hash) ([]byte, error) {
349+
func (k *ecdsaPublicKey) Thumbprint(hash crypto.Hash) ([]byte, error) {
309350
k.mu.RLock()
310351
defer k.mu.RUnlock()
311352

312353
var key ecdsa.PublicKey
313-
if err := Export(&k, &key); err != nil {
354+
if err := Export(k, &key); err != nil {
314355
return nil, fmt.Errorf(`failed to export ecdsa.PublicKey for thumbprint generation: %w`, err)
315356
}
316357

@@ -329,12 +370,12 @@ func (k ecdsaPublicKey) Thumbprint(hash crypto.Hash) ([]byte, error) {
329370

330371
// Thumbprint returns the JWK thumbprint using the indicated
331372
// hashing algorithm, according to RFC 7638
332-
func (k ecdsaPrivateKey) Thumbprint(hash crypto.Hash) ([]byte, error) {
373+
func (k *ecdsaPrivateKey) Thumbprint(hash crypto.Hash) ([]byte, error) {
333374
k.mu.RLock()
334375
defer k.mu.RUnlock()
335376

336377
var key ecdsa.PrivateKey
337-
if err := Export(&k, &key); err != nil {
378+
if err := Export(k, &key); err != nil {
338379
return nil, fmt.Errorf(`failed to export ecdsa.PrivateKey for thumbprint generation: %w`, err)
339380
}
340381

0 commit comments

Comments
 (0)