diff --git a/.releaserc.js b/.releaserc.js index 2847184..6500ace 100644 --- a/.releaserc.js +++ b/.releaserc.js @@ -5,4 +5,4 @@ module.exports = { "@semantic-release/release-notes-generator", "@semantic-release/github" ] - }; +}; diff --git a/go.sum b/go.sum index b3d1da2..4ff0c39 100644 --- a/go.sum +++ b/go.sum @@ -50,9 +50,13 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= +github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -95,6 +99,7 @@ github.com/google/go-github/v31 v31.0.0 h1:JJUxlP9lFK+ziXKimTCprajMApV1ecWD4NB6C github.com/google/go-github/v31 v31.0.0/go.mod h1:NQPZol8/1sMoWYGN2yaALIBytu17gAWfhbweiEed3pM= github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -120,6 +125,7 @@ github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= @@ -129,21 +135,30 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rs/cors v1.8.0 h1:P2KMzcFwrPoSjkF1WLRPsp3UMLyql8L4v9hQpVeK5so= +github.com/rs/cors v1.8.0/go.mod h1:EBwu+T5AvHOcXwvZIkQFjUN6s8Czyqw12GL/Y0tUyRM= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -244,6 +259,7 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -403,6 +419,8 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= +gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/pkg/client/rp/jwks.go b/pkg/client/rp/jwks.go index 98ed501..4062ab4 100644 --- a/pkg/client/rp/jwks.go +++ b/pkg/client/rp/jwks.go @@ -3,26 +3,41 @@ package rp import ( "context" "encoding/json" - "errors" "fmt" "net/http" "sync" "github.com/caos/oidc/pkg/utils" - "gopkg.in/square/go-jose.v2" "github.com/caos/oidc/pkg/oidc" ) -func NewRemoteKeySet(client *http.Client, jwksURL string) oidc.KeySet { - return &remoteKeySet{httpClient: client, jwksURL: jwksURL} +func NewRemoteKeySet(client *http.Client, jwksURL string, opts ...func(*remoteKeySet)) oidc.KeySet { + keyset := &remoteKeySet{httpClient: client, jwksURL: jwksURL} + for _, opt := range opts { + opt(keyset) + } + return keyset +} + +//SkipRemoteCheck will suppress checking for new remote keys if signature validation fails with cached keys +//and no kid header is set in the JWT +// +//this might be handy to save some unnecessary round trips in cases where the JWT does not contain a kid header and +//there is only a single remote key +//please notice that remote keys will then only be fetched if cached keys are empty +func SkipRemoteCheck() func(set *remoteKeySet) { + return func(set *remoteKeySet) { + set.skipRemoteCheck = true + } } type remoteKeySet struct { - jwksURL string - httpClient *http.Client - defaultAlg string + jwksURL string + httpClient *http.Client + defaultAlg string + skipRemoteCheck bool // guard all other fields mu sync.Mutex @@ -72,23 +87,67 @@ func (r *remoteKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSig if alg == "" { alg = r.defaultAlg } - keys := r.keysFromCache() - key, ok := oidc.FindKey(keyID, oidc.KeyUseSignature, alg, keys...) - if ok && keyID != "" { - payload, err := jws.Verify(&key) - return payload, err + payload, err := r.verifySignatureCached(jws, keyID, alg) + if payload != nil { + return payload, nil } + if err != nil { + return nil, err + } + return r.verifySignatureRemote(ctx, jws, keyID, alg) +} +//verifySignatureCached checks for a matching key in the cached key list +// +//if there is only one possible, it tries to verify the signature and will return the payload if successful +// +//it only returns an error if signature validation fails and keys exactMatch which is if either: +// - both kid are empty and skipRemoteCheck is set to true +// - or both (JWT and JWK) kid are equal +// +//otherwise it will return no error (so remote keys will be loaded) +func (r *remoteKeySet) verifySignatureCached(jws *jose.JSONWebSignature, keyID, alg string) ([]byte, error) { + keys := r.keysFromCache() + if len(keys) == 0 { + return nil, nil + } + key, err := oidc.FindMatchingKey(keyID, oidc.KeyUseSignature, alg, keys...) + if err != nil { + //no key / multiple found, try with remote keys + return nil, nil //nolint:nilerr + } + payload, err := jws.Verify(&key) + if payload != nil { + return payload, nil + } + if !r.exactMatch(key.KeyID, keyID) { + //no exact key match, try getting better match with remote keys + return nil, nil + } + return nil, fmt.Errorf("signature verification failed: %w", err) +} + +func (r *remoteKeySet) exactMatch(jwkID, jwsID string) bool { + if jwkID == "" && jwsID == "" { + return r.skipRemoteCheck + } + return jwkID == jwsID +} + +func (r *remoteKeySet) verifySignatureRemote(ctx context.Context, jws *jose.JSONWebSignature, keyID, alg string) ([]byte, error) { keys, err := r.keysFromRemote(ctx) if err != nil { - return nil, fmt.Errorf("fetching keys %v", err) + return nil, fmt.Errorf("unable to fetch key for signature validation: %w", err) } - key, ok = oidc.FindKey(keyID, oidc.KeyUseSignature, alg, keys...) - if ok { - payload, err := jws.Verify(&key) - return payload, err + key, err := oidc.FindMatchingKey(keyID, oidc.KeyUseSignature, alg, keys...) + if err != nil { + return nil, fmt.Errorf("unable to validate signature: %w", err) } - return nil, errors.New("invalid key") + payload, err := jws.Verify(&key) + if err != nil { + return nil, fmt.Errorf("signature verification failed: %w", err) + } + return payload, nil } func (r *remoteKeySet) keysFromCache() (keys []jose.JSONWebKey) { diff --git a/pkg/oidc/keyset.go b/pkg/oidc/keyset.go index adfffcf..3eca654 100644 --- a/pkg/oidc/keyset.go +++ b/pkg/oidc/keyset.go @@ -5,6 +5,7 @@ import ( "crypto/ecdsa" "crypto/ed25519" "crypto/rsa" + "errors" "gopkg.in/square/go-jose.v2" ) @@ -13,6 +14,11 @@ const ( KeyUseSignature = "sig" ) +var ( + ErrKeyMultiple = errors.New("multiple possible keys match") + ErrKeyNone = errors.New("no possible keys matches") +) + //KeySet represents a set of JSON Web Keys // - remotely fetch via discovery and jwks_uri -> `remoteKeySet` // - held by the OP itself in storage -> `openIDKeySet` @@ -39,20 +45,38 @@ func GetKeyIDAndAlg(jws *jose.JSONWebSignature) (string, string) { //will return the key immediately if matches exact (id, usage, type) // //will return false none or multiple match +// +//deprecated: use FindMatchingKey which will return an error (more specific) instead of just a bool +//moved implementation already to FindMatchingKey func FindKey(keyID, use, expectedAlg string, keys ...jose.JSONWebKey) (jose.JSONWebKey, bool) { + key, err := FindMatchingKey(keyID, use, expectedAlg, keys...) + return key, err == nil +} + +//FindMatchingKey searches the given JSON Web Keys for the requested key ID, usage and key type +// +//will return the key immediately if matches exact (id, usage, type) +// +//will return a specific error if none (ErrKeyNone) or multiple (ErrKeyMultiple) match +func FindMatchingKey(keyID, use, expectedAlg string, keys ...jose.JSONWebKey) (key jose.JSONWebKey, err error) { var validKeys []jose.JSONWebKey - for _, key := range keys { - if key.KeyID == keyID && key.Use == use && algToKeyType(key.Key, expectedAlg) { - if keyID != "" { - return key, true + for _, k := range keys { + if k.Use == use && algToKeyType(k.Key, expectedAlg) { + if k.KeyID == keyID && keyID != "" { + return k, nil + } + if k.KeyID == "" || keyID == "" { + validKeys = append(validKeys, k) } - validKeys = append(validKeys, key) } } if len(validKeys) == 1 { - return validKeys[0], true + return validKeys[0], nil } - return jose.JSONWebKey{}, false + if len(validKeys) > 1 { + return key, ErrKeyMultiple + } + return key, ErrKeyNone } func algToKeyType(key interface{}, alg string) bool { diff --git a/pkg/oidc/keyset_test.go b/pkg/oidc/keyset_test.go new file mode 100644 index 0000000..802edec --- /dev/null +++ b/pkg/oidc/keyset_test.go @@ -0,0 +1,319 @@ +package oidc + +import ( + "crypto/rsa" + "errors" + "reflect" + "testing" + + "gopkg.in/square/go-jose.v2" +) + +func TestFindKey(t *testing.T) { + type args struct { + keyID string + use string + expectedAlg string + keys []jose.JSONWebKey + } + type res struct { + key jose.JSONWebKey + err error + } + tests := []struct { + name string + args args + res res + }{ + { + "no keys, ErrKeyNone", + args{ + keyID: "", + use: KeyUseSignature, + expectedAlg: "RS256", + keys: nil, + }, + res{ + key: jose.JSONWebKey{}, + err: ErrKeyNone, + }, + }, + { + "single key enc, ErrKeyNone", + args{ + keyID: "", + use: KeyUseSignature, + expectedAlg: "RS256", + keys: []jose.JSONWebKey{ + { + Use: "enc", + Key: &rsa.PublicKey{}, + }, + }, + }, + res{ + key: jose.JSONWebKey{}, + err: ErrKeyNone, + }, + }, + { + "single key wrong algorithm, ErrKeyNone", + args{ + keyID: "", + use: KeyUseSignature, + expectedAlg: "RS256", + keys: []jose.JSONWebKey{ + { + Use: "sig", + Key: &rsa.PrivateKey{}, + }, + }, + }, + res{ + key: jose.JSONWebKey{}, + err: ErrKeyNone, + }, + }, + { + "single key no kid, no jwt kid, match", + args{ + keyID: "", + use: KeyUseSignature, + expectedAlg: "RS256", + keys: []jose.JSONWebKey{ + { + Use: "sig", + Key: &rsa.PublicKey{}, + }, + }, + }, + res{ + key: jose.JSONWebKey{ + Use: "sig", + Key: &rsa.PublicKey{}, + }, + err: nil, + }, + }, + { + "single key kid, jwt no kid, match", + args{ + keyID: "", + use: KeyUseSignature, + expectedAlg: "RS256", + keys: []jose.JSONWebKey{ + { + Use: "sig", + KeyID: "id", + Key: &rsa.PublicKey{}, + }, + }, + }, + res{ + key: jose.JSONWebKey{ + Use: "sig", + KeyID: "id", + Key: &rsa.PublicKey{}, + }, + err: nil, + }, + }, + { + "single key no kid, jwt with kid, match", + args{ + keyID: "id", + use: KeyUseSignature, + expectedAlg: "RS256", + keys: []jose.JSONWebKey{ + { + Use: "sig", + Key: &rsa.PublicKey{}, + }, + }, + }, + res{ + key: jose.JSONWebKey{ + Use: "sig", + Key: &rsa.PublicKey{}, + }, + err: nil, + }, + }, + { + "single key wrong kid, ErrKeyNone", + args{ + keyID: "id", + use: KeyUseSignature, + expectedAlg: "RS256", + keys: []jose.JSONWebKey{ + { + Use: "sig", + KeyID: "id2", + Key: &rsa.PublicKey{}, + }, + }, + }, + res{ + key: jose.JSONWebKey{}, + err: ErrKeyNone, + }, + }, + { + "multiple keys no kid, jwt no kid, ErrKeyMultiple", + args{ + keyID: "", + use: KeyUseSignature, + expectedAlg: "RS256", + keys: []jose.JSONWebKey{ + { + Use: "sig", + Key: &rsa.PublicKey{}, + }, + { + Use: "sig", + Key: &rsa.PublicKey{}, + }, + }, + }, + res{ + key: jose.JSONWebKey{}, + err: ErrKeyMultiple, + }, + }, + { + "multiple keys with kid, jwt no kid, ErrKeyMultiple", + args{ + keyID: "", + use: KeyUseSignature, + expectedAlg: "RS256", + keys: []jose.JSONWebKey{ + { + Use: "sig", + KeyID: "id1", + Key: &rsa.PublicKey{}, + }, + { + Use: "sig", + KeyID: "id2", + Key: &rsa.PublicKey{}, + }, + }, + }, + res{ + key: jose.JSONWebKey{}, + err: ErrKeyMultiple, + }, + }, + { + "multiple keys, single sig key, jwt no kid, match", + args{ + keyID: "", + use: KeyUseSignature, + expectedAlg: "RS256", + keys: []jose.JSONWebKey{ + { + Use: "sig", + Key: &rsa.PublicKey{}, + }, + { + Use: "enc", + Key: &rsa.PublicKey{}, + }, + }, + }, + res{ + key: jose.JSONWebKey{ + Use: "sig", + Key: &rsa.PublicKey{}, + }, + err: nil, + }, + }, + { + "multiple keys no kid, jwt with kid, ErrKeyMultiple", + args{ + keyID: "id", + use: KeyUseSignature, + expectedAlg: "RS256", + keys: []jose.JSONWebKey{ + { + Use: "sig", + Key: &rsa.PublicKey{}, + }, + { + Use: "sig", + Key: &rsa.PublicKey{}, + }, + }, + }, + res{ + key: jose.JSONWebKey{}, + err: ErrKeyMultiple, + }, + }, + { + "multiple keys with kid, jwt with kid, match", + args{ + keyID: "id1", + use: KeyUseSignature, + expectedAlg: "RS256", + keys: []jose.JSONWebKey{ + { + Use: "sig", + KeyID: "id1", + Key: &rsa.PublicKey{}, + }, + { + Use: "sig", + KeyID: "id2", + Key: &rsa.PublicKey{}, + }, + }, + }, + res{ + key: jose.JSONWebKey{ + Use: "sig", + KeyID: "id1", + Key: &rsa.PublicKey{}, + }, + err: nil, + }, + }, + { + "multiple keys, single sig key, jwt with kid, match", + args{ + keyID: "id1", + use: KeyUseSignature, + expectedAlg: "RS256", + keys: []jose.JSONWebKey{ + { + Use: "sig", + Key: &rsa.PublicKey{}, + }, + { + Use: "enc", + Key: &rsa.PublicKey{}, + }, + }, + }, + res{ + key: jose.JSONWebKey{ + Use: "sig", + Key: &rsa.PublicKey{}, + }, + err: nil, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := FindMatchingKey(tt.args.keyID, tt.args.use, tt.args.expectedAlg, tt.args.keys...) + if (tt.res.err != nil && !errors.Is(err, tt.res.err)) || (tt.res.err == nil && err != nil) { + t.Errorf("FindKey() error, got = %v, want = %v", err, tt.res.err) + } + if !reflect.DeepEqual(got, tt.res.key) { + t.Errorf("FindKey() got = %v, want %v", got, tt.res.key) + } + }) + } +} diff --git a/pkg/oidc/verifier.go b/pkg/oidc/verifier.go index f8470b5..4284d17 100644 --- a/pkg/oidc/verifier.go +++ b/pkg/oidc/verifier.go @@ -39,6 +39,7 @@ var ( ErrSignatureMultiple = errors.New("id_token contains multiple signatures") ErrSignatureUnsupportedAlg = errors.New("signature algorithm not supported") ErrSignatureInvalidPayload = errors.New("signature does not match Payload") + ErrSignatureInvalid = errors.New("invalid signature") ErrExpired = errors.New("token has expired") ErrIatMissing = errors.New("issuedAt of token is missing") ErrIatInFuture = errors.New("issuedAt of token is in the future") @@ -143,7 +144,7 @@ func CheckSignature(ctx context.Context, token string, payload []byte, claims Cl signedPayload, err := set.VerifySignature(ctx, jws) if err != nil { - return err + return fmt.Errorf("%w (%v)", ErrSignatureInvalid, err) } if !bytes.Equal(signedPayload, payload) { diff --git a/pkg/op/op.go b/pkg/op/op.go index 772b5f7..3841227 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -2,7 +2,7 @@ package op import ( "context" - "errors" + "fmt" "net/http" "time" @@ -280,12 +280,12 @@ type openIDKeySet struct { func (o *openIDKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) ([]byte, error) { keySet, err := o.Storage.GetKeySet(ctx) if err != nil { - return nil, errors.New("error fetching keys") + return nil, fmt.Errorf("error fetching keys: %w", err) } keyID, alg := oidc.GetKeyIDAndAlg(jws) - key, ok := oidc.FindKey(keyID, oidc.KeyUseSignature, alg, keySet.Keys...) - if !ok { - return nil, errors.New("invalid kid") + key, err := oidc.FindMatchingKey(keyID, oidc.KeyUseSignature, alg, keySet.Keys...) + if err != nil { + return nil, fmt.Errorf("invalid signature: %w", err) } return jws.Verify(&key) }