Skip to content

Commit f9049c9

Browse files
committed
oidc: ignore JWKs with unknown signing algorithms rather than failing
Fixes #490
1 parent 2f178e0 commit f9049c9

3 files changed

Lines changed: 123 additions & 1 deletion

File tree

oidc/jose.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,11 @@ var allAlgs = []jose.SignatureAlgorithm{
3030
jose.PS512,
3131
jose.EdDSA,
3232
}
33+
34+
var supportedJOSEAlgs = map[string]bool{}
35+
36+
func init() {
37+
for _, alg := range allAlgs {
38+
supportedJOSEAlgs[string(alg)] = true
39+
}
40+
}

oidc/jwks.go

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"crypto/ecdsa"
77
"crypto/ed25519"
88
"crypto/rsa"
9+
"encoding/json"
910
"errors"
1011
"fmt"
1112
"io"
@@ -235,6 +236,55 @@ func (r *RemoteKeySet) keysFromRemote(ctx context.Context) ([]jose.JSONWebKey, e
235236
}
236237
}
237238

239+
// jwkJSON implements custom logic for unmarshaling JSON Web Key Sets.
240+
type jwkJSON struct {
241+
Keys []jose.JSONWebKey
242+
}
243+
244+
func (j *jwkJSON) UnmarshalJSON(data []byte) error {
245+
var raw struct {
246+
Keys []json.RawMessage `json:"keys"`
247+
}
248+
if err := json.Unmarshal(data, &raw); err != nil {
249+
return err
250+
}
251+
// For each key, attempt to determine if the algorithm is supported before
252+
// unmarshaling the key. This allows us to ignore keys with unsupported
253+
// algorithms, rather than failing to load the entire set.
254+
for _, key := range raw.Keys {
255+
var metadata struct {
256+
Alg string `json:"alg"`
257+
}
258+
if err := json.Unmarshal(key, &metadata); err != nil {
259+
return err
260+
}
261+
if metadata.Alg != "" {
262+
// Algorithms are technically optional, but if the key advertises an
263+
// algorithm we don't support, ignore it rather than failing to load
264+
// the entire key set.
265+
//
266+
// https://datatracker.ietf.org/doc/html/rfc7517#section-4.4
267+
//
268+
// This skip is currently implemented due to secp256k1, which some
269+
// providers use for signing with "ES256K". While we could also
270+
// ignore the curve, this seems like a more general check in case
271+
// providers start throwing in post-quantum algorithims or something
272+
// like that.
273+
//
274+
// https://github.com/coreos/go-oidc/issues/490
275+
if !supportedJOSEAlgs[metadata.Alg] {
276+
continue
277+
}
278+
}
279+
var jwk jose.JSONWebKey
280+
if err := json.Unmarshal(key, &jwk); err != nil {
281+
return err
282+
}
283+
j.Keys = append(j.Keys, jwk)
284+
}
285+
return nil
286+
}
287+
238288
func (r *RemoteKeySet) updateKeys() ([]jose.JSONWebKey, error) {
239289
req, err := http.NewRequest("GET", r.jwksURL, nil)
240290
if err != nil {
@@ -257,7 +307,7 @@ func (r *RemoteKeySet) updateKeys() ([]jose.JSONWebKey, error) {
257307
return nil, fmt.Errorf("oidc: get keys failed: %s %s", resp.Status, body)
258308
}
259309

260-
var keySet jose.JSONWebKeySet
310+
var keySet jwkJSON
261311
err = unmarshalResp(resp, body, &keySet)
262312
if err != nil {
263313
return nil, fmt.Errorf("oidc: failed to decode keys: %v %s", err, body)

oidc/jwks_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,3 +362,67 @@ func BenchmarkVerify(b *testing.B) {
362362
}
363363
}
364364
}
365+
366+
func TestJWKParseWithIgnoredKeys(t *testing.T) {
367+
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
368+
if err != nil {
369+
t.Fatal(err)
370+
}
371+
pubKey := jose.JSONWebKey{
372+
Key: priv.Public(),
373+
Use: "sig",
374+
Algorithm: string(jose.ES256),
375+
KeyID: "key1",
376+
}
377+
pubRaw, err := json.Marshal(pubKey)
378+
if err != nil {
379+
t.Fatal(err)
380+
}
381+
privKey := jose.JSONWebKey{
382+
Key: priv,
383+
Use: "sig",
384+
Algorithm: string(jose.ES256),
385+
KeyID: "key1",
386+
}
387+
388+
hf := func(w http.ResponseWriter, r *http.Request) {
389+
w.Header().Set("Content-Type", "application/json")
390+
io.WriteString(w, `{
391+
"keys": [
392+
`+string(pubRaw)+`,
393+
{
394+
"alg": "ES256K",
395+
"kty": "EC",
396+
"x": "vsk5i5YJu8H_VPL7DWTgVGXBPrqgkyNmYvfgOrVut38",
397+
"y": "Mrg56tBhVeorHPXK1LbTX2jP7rEqOHIatM96HFzVMIU",
398+
"crv": "secp256k1",
399+
"kid": "oidc-es256k-1",
400+
"use": "sig"
401+
}
402+
]
403+
}`)
404+
}
405+
406+
ks := NewRemoteKeySet(t.Context(), httptest.NewServer(http.HandlerFunc(hf)).URL)
407+
408+
signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: &privKey}, nil)
409+
if err != nil {
410+
t.Fatal(err)
411+
}
412+
jws, err := signer.Sign([]byte("payload"))
413+
if err != nil {
414+
t.Fatal(err)
415+
}
416+
serialized, err := jws.CompactSerialize()
417+
if err != nil {
418+
t.Fatal(err)
419+
}
420+
421+
got, err := ks.VerifySignature(t.Context(), serialized)
422+
if err != nil {
423+
t.Fatalf("failed to verify signature: %v", err)
424+
}
425+
if string(got) != "payload" {
426+
t.Errorf("expected payload %q got %q", "payload", string(got))
427+
}
428+
}

0 commit comments

Comments
 (0)