diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index e0d1df4b3c..0000000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,60 +0,0 @@ -version: 2.1 - -workflows: - tests: - jobs: - - build - - report: - requires: - - build -orbs: - qlty: qltysh/qlty-orb@0.0 -jobs: - build: - parallelism: 8 - docker: - - image: cimg/go:1.26 - steps: - - checkout - - - restore_cache: - keys: - - go-mod-v2-{{ checksum "go.sum" }} - - - run: go get -t -v ./... - - - save_cache: - key: go-mod-v2-{{ checksum "go.sum" }} - paths: - - "/go/pkg/mod" - - - run: mkdir cov - - run: go test -p 1 -v $(go list ./... | circleci tests split) -race -coverprofile=cov/c_raw_$CIRCLE_NODE_INDEX.out - - persist_to_workspace: - root: . - paths: - - cov - - report: - docker: - - image: cimg/go:1.26 - steps: - - checkout - - attach_workspace: - at: . - - run: - name: Merge test files - command: | - cat "cov/c_raw_0.out" >> c_raw.out - for f in $(seq 1 7) - do - tail -n +2 "cov/c_raw_$f.out" >> c_raw.out - done - - run: - name: Remove test, mock and generated code - command: | - cat c_raw.out | grep -v generated | grep -v mock | grep -v test > c.out - # Qlty Coverage Orb - - qlty/coverage_publish: - files: c.out - format: coverprofile diff --git a/.github/workflows/build-images.yaml b/.github/workflows/build-images.yaml index 40add276be..48981162e3 100644 --- a/.github/workflows/build-images.yaml +++ b/.github/workflows/build-images.yaml @@ -75,7 +75,9 @@ jobs: with: context: . file: Dockerfile - platforms: linux/amd64,linux/arm64 + # On PRs we only verify the build doesn't break; arm64 under QEMU + # is ~5-8x slower and adds no signal there. Multi-arch on push/tag. + platforms: ${{ github.event_name == 'pull_request' && 'linux/amd64' || 'linux/amd64,linux/arm64' }} push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.docker_meta.outputs.tags }} build-args: | diff --git a/.github/workflows/e2e-tests.yaml b/.github/workflows/e2e-tests.yaml index 0168956f99..0ade2dd8eb 100644 --- a/.github/workflows/e2e-tests.yaml +++ b/.github/workflows/e2e-tests.yaml @@ -60,6 +60,8 @@ jobs: platforms: linux/amd64 push: ${{ github.event_name == 'pull_request' || github.event_name == 'push' }} tags: ghcr.io/nuts-foundation/nuts-node-ci:${{ env.SHA }} + cache-from: type=gha,scope=e2e + cache-to: type=gha,scope=e2e,mode=max secrets: | GIT_AUTH_TOKEN=${{ secrets.PACKAGE_SECRET }} @@ -72,6 +74,8 @@ jobs: platforms: linux/amd64 push: ${{ github.event_name == 'pull_request' || github.event_name == 'push' }} tags: ghcr.io/nuts-foundation/nuts-node-ci:${{ env.SHA }} + cache-from: type=gha,scope=e2e + cache-to: type=gha,scope=e2e,mode=max - name: Run E2E tests run: | diff --git a/.github/workflows/go-test.yaml b/.github/workflows/go-test.yaml new file mode 100644 index 0000000000..a53d1bcca2 --- /dev/null +++ b/.github/workflows/go-test.yaml @@ -0,0 +1,41 @@ +name: 'Go test' + +on: + push: + branches: + - 'master' + - 'V*' + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version-file: 'go.mod' + + - name: Test + run: go test ./... -race -coverprofile=c_raw.out + + - name: Filter coverage + run: grep -v generated c_raw.out | grep -v mock | grep -v test > c.out + + - name: Upload coverage to Qlty + # Skip on PRs from forks: GitHub does not grant id-token: write to + # cross-repository workflow runs, so the OIDC upload cannot authenticate. + if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository + uses: qltysh/qlty-action/coverage@a19242102d17e497f437d7466aa01b528537e899 # v2.2.0 + with: + oidc: true + files: c.out + format: coverprofile diff --git a/Dockerfile b/Dockerfile index 5ce0e91253..6133712cce 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # golang alpine -FROM golang:1.26.1-alpine AS builder +FROM golang:1.26.5-alpine AS builder ARG TARGETARCH ARG TARGETOS @@ -21,7 +21,7 @@ COPY . . RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags="-w -s -X 'github.com/nuts-foundation/nuts-node/core.GitCommit=${GIT_COMMIT}' -X 'github.com/nuts-foundation/nuts-node/core.GitBranch=${GIT_BRANCH}' -X 'github.com/nuts-foundation/nuts-node/core.GitVersion=${GIT_VERSION}'" -o /opt/nuts/nuts # alpine -FROM alpine:3.23.3 +FROM alpine:3.23.4 RUN apk update \ && apk add --no-cache \ tzdata \ diff --git a/auth/api/iam/api.go b/auth/api/iam/api.go index 339d4b0086..b2b5e023bc 100644 --- a/auth/api/iam/api.go +++ b/auth/api/iam/api.go @@ -38,8 +38,8 @@ import ( "time" "github.com/labstack/echo/v4" - "github.com/lestrrat-go/jwx/v2/jwk" - "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/lestrrat-go/jwx/v3/jwk" + "github.com/lestrrat-go/jwx/v3/jwt" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/nuts-node/audit" "github.com/nuts-foundation/nuts-node/auth" @@ -410,7 +410,11 @@ func (r Wrapper) introspectAccessToken(input string) (*ExtendedTokenIntrospectio // SHA256 hashing won't fail. var cnf *Cnf if token.DPoP != nil { - hash, _ := token.DPoP.Headers.JWK().Thumbprint(crypto.SHA256) + key, ok := token.DPoP.Headers.JWK() + if !ok { + return nil, errors.New("DPoP header is missing the jwk") + } + hash, _ := key.Thumbprint(crypto.SHA256) base64Hash := base64.RawURLEncoding.EncodeToString(hash) cnf = &Cnf{Jkt: base64Hash} } @@ -661,7 +665,7 @@ func (r Wrapper) OpenIDConfiguration(ctx context.Context, request OpenIDConfigur } } // create JWK and add to set - jwkKey, err := jwk.FromRaw(key) + jwkKey, err := jwk.Import(key) if err != nil { return nil, oauth.OAuth2Error{ Code: oauth.ServerError, diff --git a/auth/api/iam/api_test.go b/auth/api/iam/api_test.go index 35efca16da..679af97589 100644 --- a/auth/api/iam/api_test.go +++ b/auth/api/iam/api_test.go @@ -33,9 +33,9 @@ import ( "time" "github.com/labstack/echo/v4" - "github.com/lestrrat-go/jwx/v2/jwa" - "github.com/lestrrat-go/jwx/v2/jws" - "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/lestrrat-go/jwx/v3/jwa" + "github.com/lestrrat-go/jwx/v3/jws" + "github.com/lestrrat-go/jwx/v3/jwt" ssi "github.com/nuts-foundation/go-did" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/go-did/vc" @@ -1503,7 +1503,7 @@ func createIssuerCredential(issuerDID did.DID, holderDID did.DID) *vc.Verifiable for key, val := range claims { request.Set(key, val) } - sign, err := jwt.Sign(request, jwt.WithKey(jwa.ES256, privateKey, jws.WithProtectedHeaders(hdrs))) + sign, err := jwt.Sign(request, jwt.WithKey(jwa.ES256(), privateKey, jws.WithProtectedHeaders(hdrs))) return string(sign), err } diff --git a/auth/api/iam/dpop.go b/auth/api/iam/dpop.go index 6de6e379bb..b690a7c45a 100644 --- a/auth/api/iam/dpop.go +++ b/auth/api/iam/dpop.go @@ -76,8 +76,8 @@ func (r Wrapper) ValidateDPoPProof(_ context.Context, request ValidateDPoPProofR return ValidateDPoPProof200JSONResponse{Reason: &reason}, nil } // check if ath claim matches hash of access_token - ath, ok := dpopToken.Token.Get(dpop.ATHKey) - if !ok { + var ath string + if err := dpopToken.Token.Get(dpop.ATHKey, &ath); err != nil { reason := "missing ath claim" return ValidateDPoPProof200JSONResponse{Reason: &reason}, nil } @@ -87,13 +87,14 @@ func (r Wrapper) ValidateDPoPProof(_ context.Context, request ValidateDPoPProofR return ValidateDPoPProof200JSONResponse{Reason: &reason}, nil } // check if the jti is already used, if not add it to the store for the duration of the access token lifetime + jti, _ := dpopToken.Token.JwtID() var target struct{} - if err := r.useNonceOnceStore().Get(dpopToken.Token.JwtID(), &target); err != nil { + if err := r.useNonceOnceStore().Get(jti, &target); err != nil { if !errors.Is(err, storage.ErrNotFound) { log.Logger().WithError(err).Error("ValidateDPoPProof: failed to retrieve jti usage state") return nil, err } - if err := r.useNonceOnceStore().Put(dpopToken.Token.JwtID(), target); err != nil { + if err := r.useNonceOnceStore().Put(jti, target); err != nil { log.Logger().WithError(err).Error("ValidateDPoPProof: failed to store jti usage state") return nil, err } diff --git a/auth/api/iam/dpop_test.go b/auth/api/iam/dpop_test.go index 1b55d9b252..1c9f54c160 100644 --- a/auth/api/iam/dpop_test.go +++ b/auth/api/iam/dpop_test.go @@ -29,7 +29,7 @@ import ( "net/http" "testing" - "github.com/lestrrat-go/jwx/v2/jwa" + "github.com/lestrrat-go/jwx/v3/jwa" ssi "github.com/nuts-foundation/go-did" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/nuts-node/auth/oauth" @@ -172,7 +172,7 @@ func TestWrapper_ValidateDPoPProof(t *testing.T) { require.NoError(t, err) require.IsType(t, ValidateDPoPProof200JSONResponse{}, resp) assert.False(t, resp.(ValidateDPoPProof200JSONResponse).Valid) - assert.Equal(t, "failed to parse DPoP header: invalid DPoP token\ninvalid compact serialization format: invalid number of segments", *resp.(ValidateDPoPProof200JSONResponse).Reason) + assert.Equal(t, "failed to parse DPoP header: invalid DPoP token\njws.ParseString: failed to parse string: jws.Parse: failed to parse compact format: jws.Parse: invalid compact serialization format: jwsbb: invalid number of segments", *resp.(ValidateDPoPProof200JSONResponse).Reason) }) t.Run("invalid accestoken", func(t *testing.T) { ctx := newTestClient(t) @@ -188,7 +188,8 @@ func TestWrapper_ValidateDPoPProof(t *testing.T) { }) t.Run("already used once", func(t *testing.T) { ctx := newTestClient(t) - _ = ctx.client.useNonceOnceStore().Put(dpopProof.Token.JwtID(), struct{}{}) + jti, _ := dpopProof.Token.JwtID() + _ = ctx.client.useNonceOnceStore().Put(jti, struct{}{}) resp, err := ctx.client.ValidateDPoPProof(nil, request) @@ -229,9 +230,10 @@ func newSignedTestDPoP() (*dpop.DPoP, *dpop.DPoP, string) { withProof := newTestDPoP() keyPair, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) _ = withProof.GenerateProof("token") - _, _ = withProof.Sign("kid", keyPair, jwa.ES256) - _, _ = dpopToken.Sign("kid", keyPair, jwa.ES256) - thumbprintBytes, _ := dpopToken.Headers.JWK().Thumbprint(crypto2.SHA256) + _, _ = withProof.Sign("kid", keyPair, jwa.ES256()) + _, _ = dpopToken.Sign("kid", keyPair, jwa.ES256()) + jwkKey, _ := dpopToken.Headers.JWK() + thumbprintBytes, _ := jwkKey.Thumbprint(crypto2.SHA256) thumbprint := base64.RawURLEncoding.EncodeToString(thumbprintBytes) return dpopToken, withProof, thumbprint } diff --git a/auth/api/iam/jar.go b/auth/api/iam/jar.go index 8660a86b27..a5555a103e 100644 --- a/auth/api/iam/jar.go +++ b/auth/api/iam/jar.go @@ -23,12 +23,13 @@ import ( "context" "crypto" "errors" - "github.com/lestrrat-go/jwx/v2/jwk" - "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/lestrrat-go/jwx/v3/jwk" + "github.com/lestrrat-go/jwx/v3/jwt" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/nuts-node/auth" "github.com/nuts-foundation/nuts-node/auth/oauth" cryptoNuts "github.com/nuts-foundation/nuts-node/crypto" + "github.com/nuts-foundation/nuts-node/crypto/jwx" "github.com/nuts-foundation/nuts-node/vdr/resolver" "net/url" ) @@ -158,7 +159,7 @@ func (j jar) validate(ctx context.Context, rawToken string, clientId string) (oa if err != nil { return nil, oauth.OAuth2Error{Code: oauth.InvalidRequestObject, Description: "request signature validation failed", InternalError: err} } - claimsAsMap, err := token.AsMap(ctx) + claimsAsMap, err := jwx.ClaimsAsMap(token) if err != nil { // very unlikely return nil, oauth.OAuth2Error{Code: oauth.InvalidRequestObject, Description: "invalid request parameter", InternalError: err} @@ -188,7 +189,7 @@ func compareThumbprint(configurationKey jwk.Key, publicKey crypto.PublicKey) err if err != nil { return err } - signerKey, err := jwk.FromRaw(publicKey) + signerKey, err := jwk.Import(publicKey) if err != nil { return err } diff --git a/auth/api/iam/jar_test.go b/auth/api/iam/jar_test.go index 5072dcdea9..592cbe2113 100644 --- a/auth/api/iam/jar_test.go +++ b/auth/api/iam/jar_test.go @@ -23,14 +23,14 @@ import ( "crypto" "errors" "fmt" - "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v3/jwk" "github.com/nuts-foundation/nuts-node/crypto/storage/spi" "github.com/nuts-foundation/nuts-node/test" "testing" - "github.com/lestrrat-go/jwx/v2/jwa" - "github.com/lestrrat-go/jwx/v2/jws" - "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/lestrrat-go/jwx/v3/jwa" + "github.com/lestrrat-go/jwx/v3/jws" + "github.com/lestrrat-go/jwx/v3/jwt" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/nuts-node/auth" "github.com/nuts-foundation/nuts-node/auth/client/iam" @@ -109,7 +109,7 @@ func TestJar_Parse(t *testing.T) { // setup did document and keys privateKey, _ := spi.GenerateKeyPair() kid := fmt.Sprintf("%s#%s", holderDID.String(), "key") - jwkKey, _ := jwk.FromRaw(privateKey.Public()) + jwkKey, _ := jwk.Import(privateKey.Public()) jwkKey.Set(jwk.KeyIDKey, kid) jwkSet := jwk.NewSet() _ = jwkSet.AddKey(jwkKey) @@ -306,7 +306,7 @@ func TestJar_Parse(t *testing.T) { t.Run("error - openID configuration key mismatch", func(t *testing.T) { ctx := newJarTestCtx(t) alternateKey, _ := spi.GenerateKeyPair() - jwkKey, _ := jwk.FromRaw(alternateKey.Public()) + jwkKey, _ := jwk.Import(alternateKey.Public()) jwkKey.Set(jwk.KeyIDKey, kid) jwkSet := jwk.NewSet() _ = jwkSet.AddKey(jwkKey) @@ -335,7 +335,7 @@ func createSignedRequestObject(t testing.TB, kid string, privateKey crypto.Priva } headers := jws.NewHeaders() require.NoError(t, headers.Set(jws.KeyIDKey, kid)) - return jwt.Sign(request, jwt.WithKey(jwa.ES256, privateKey, jws.WithProtectedHeaders(headers))) + return jwt.Sign(request, jwt.WithKey(jwa.ES256(), privateKey, jws.WithProtectedHeaders(headers))) } type testJarCtx struct { diff --git a/auth/api/iam/metadata.go b/auth/api/iam/metadata.go index ec58fac866..578119813b 100644 --- a/auth/api/iam/metadata.go +++ b/auth/api/iam/metadata.go @@ -19,12 +19,12 @@ package iam import ( - "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v3/jwk" "net/url" "strings" "time" - "github.com/lestrrat-go/jwx/v2/jwa" + "github.com/lestrrat-go/jwx/v3/jwa" "github.com/nuts-foundation/nuts-node/auth/oauth" "github.com/nuts-foundation/nuts-node/core" "github.com/nuts-foundation/nuts-node/core/to" @@ -67,10 +67,10 @@ func staticAuthorizationServerMetadata() oauth.AuthorizationServerMetadata { ClientIdSchemesSupported: clientIdSchemesSupported, ResponseTypesSupported: []string{oauth.VPTokenResponseType}, VPFormatsSupported: map[string]map[string][]string{ - "jwt_vp_json": {"alg_values_supported": []string{string(jwa.ES256)}}, - "jwt_vc_json": {"alg_values_supported": []string{string(jwa.ES256)}}, + "jwt_vp_json": {"alg_values_supported": []string{jwa.ES256().String()}}, + "jwt_vc_json": {"alg_values_supported": []string{jwa.ES256().String()}}, }, - RequestObjectSigningAlgValuesSupported: []string{string(jwa.ES256)}, + RequestObjectSigningAlgValuesSupported: []string{jwa.ES256().String()}, } } diff --git a/auth/api/iam/openid4vci.go b/auth/api/iam/openid4vci.go index 021c1b7463..952a42f39f 100644 --- a/auth/api/iam/openid4vci.go +++ b/auth/api/iam/openid4vci.go @@ -27,7 +27,7 @@ import ( "net/url" "time" - "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/lestrrat-go/jwx/v3/jwt" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/go-did/vc" "github.com/nuts-foundation/nuts-node/auth/oauth" diff --git a/auth/api/iam/openid4vci_test.go b/auth/api/iam/openid4vci_test.go index 957595cbc8..5f4b31ef41 100644 --- a/auth/api/iam/openid4vci_test.go +++ b/auth/api/iam/openid4vci_test.go @@ -272,7 +272,7 @@ func TestWrapper_handleOpenID4VCICallback(t *testing.T) { callback, err := ctx.client.handleOpenID4VCICallback(nil, code, &session) assert.Nil(t, callback) - assert.EqualError(t, err, "server_error - error while parsing the credential: super invalid, error: invalid JWT") + assert.EqualError(t, err, "server_error - error while parsing the credential: super invalid, error: jwt.Parse: failed to parse token: unknown payload type (payload is not JWT?)") }) t.Run("fail_verify", func(t *testing.T) { ctx := newTestClient(t) diff --git a/auth/api/iam/openid4vp.go b/auth/api/iam/openid4vp.go index 53868ed600..96f123732f 100644 --- a/auth/api/iam/openid4vp.go +++ b/auth/api/iam/openid4vp.go @@ -34,8 +34,8 @@ import ( "strings" "time" - "github.com/lestrrat-go/jwx/v2/jwk" - "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/lestrrat-go/jwx/v3/jwk" + "github.com/lestrrat-go/jwx/v3/jwt" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/go-did/vc" "github.com/nuts-foundation/nuts-node/auth/oauth" @@ -598,8 +598,7 @@ func extractChallenge(presentation vc.VerifiablePresentation) (string, error) { var nonce string switch presentation.Format() { case vc.JWTPresentationProofFormat: - nonceRaw, _ := presentation.JWT().Get("nonce") - nonce, _ = nonceRaw.(string) + _ = presentation.JWT().Get("nonce", &nonce) case vc.JSONLDPresentationProofFormat: proof, err := credential.ParseLDProof(presentation) if err != nil { diff --git a/auth/api/iam/openid4vp_test.go b/auth/api/iam/openid4vp_test.go index bcc4137bdc..66859269dd 100644 --- a/auth/api/iam/openid4vp_test.go +++ b/auth/api/iam/openid4vp_test.go @@ -31,7 +31,7 @@ import ( "github.com/nuts-foundation/nuts-node/http/user" "github.com/nuts-foundation/nuts-node/vcr/verifier" - "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/lestrrat-go/jwx/v3/jwt" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/go-did/vc" "github.com/nuts-foundation/nuts-node/auth/oauth" diff --git a/auth/api/iam/params.go b/auth/api/iam/params.go index 8c66ad3721..181a1e0390 100644 --- a/auth/api/iam/params.go +++ b/auth/api/iam/params.go @@ -42,6 +42,13 @@ func (ssp oauthParameters) get(key string) string { if len(typedValue) == 1 { return typedValue[0] } + case []interface{}: + // JWT claims decoded from JSON (e.g. via jwx.ClaimsAsMap) carry arrays as []interface{}. + if len(typedValue) == 1 { + if str, ok := typedValue[0].(string); ok { + return str + } + } } return "" } diff --git a/auth/api/iam/params_test.go b/auth/api/iam/params_test.go new file mode 100644 index 0000000000..5035407b4b --- /dev/null +++ b/auth/api/iam/params_test.go @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2024 Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package iam + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestOauthParameters_get(t *testing.T) { + params := oauthParameters{ + "string": "value", + "stringSlice": []string{"value"}, + "interfaceSlice": []interface{}{"value"}, + "multiSlice": []interface{}{"a", "b"}, + "nonStringSlice": []interface{}{1}, + "number": 1, + } + assert.Equal(t, "value", params.get("string")) + assert.Equal(t, "value", params.get("stringSlice")) + // JWT claims decoded from JSON (e.g. the "aud" claim via jwx.ClaimsAsMap) arrive as []interface{}. + assert.Equal(t, "value", params.get("interfaceSlice")) + assert.Empty(t, params.get("multiSlice")) + assert.Empty(t, params.get("nonStringSlice")) + assert.Empty(t, params.get("number")) + assert.Empty(t, params.get("absent")) +} diff --git a/auth/api/iam/s2s_vptoken.go b/auth/api/iam/s2s_vptoken.go index c215ea4269..6e18100154 100644 --- a/auth/api/iam/s2s_vptoken.go +++ b/auth/api/iam/s2s_vptoken.go @@ -199,8 +199,7 @@ func extractNonce(presentation vc.VerifiablePresentation) (string, error) { var nonce string switch presentation.Format() { case vc.JWTPresentationProofFormat: - nonceRaw, _ := presentation.JWT().Get("nonce") - nonce, _ = nonceRaw.(string) + _ = presentation.JWT().Get("nonce", &nonce) case vc.JSONLDPresentationProofFormat: proof, err := credential.ParseLDProof(presentation) if err != nil { diff --git a/auth/api/iam/s2s_vptoken_test.go b/auth/api/iam/s2s_vptoken_test.go index 7cc4504b7c..3d96af3097 100644 --- a/auth/api/iam/s2s_vptoken_test.go +++ b/auth/api/iam/s2s_vptoken_test.go @@ -32,7 +32,7 @@ import ( "testing" "time" - "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/lestrrat-go/jwx/v3/jwt" ssi "github.com/nuts-foundation/go-did" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/go-did/vc" @@ -182,7 +182,7 @@ func TestWrapper_handleS2SAccessTokenRequest(t *testing.T) { ctx := newTestClient(t) resp, err := ctx.client.handleS2SAccessTokenRequest(context.Background(), clientID, issuerSubjectID, requestedScope, submissionJSON, "[true, false]") - assert.EqualError(t, err, "invalid_request - assertion parameter is invalid: unable to parse PEX envelope as verifiable presentation: invalid JWT") + assert.EqualError(t, err, "invalid_request - assertion parameter is invalid: unable to parse PEX envelope as verifiable presentation: jwt.Parse: failed to parse token: unknown payload type (payload is not JWT?)") assert.Nil(t, resp) }) t.Run("not all VPs have the same credential subject ID", func(t *testing.T) { diff --git a/auth/api/iam/user_test.go b/auth/api/iam/user_test.go index 1edc14d76c..6c9307005c 100644 --- a/auth/api/iam/user_test.go +++ b/auth/api/iam/user_test.go @@ -28,8 +28,8 @@ import ( "testing" "time" - "github.com/lestrrat-go/jwx/v2/jwa" - "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v3/jwa" + "github.com/lestrrat-go/jwx/v3/jwk" ssi "github.com/nuts-foundation/go-did" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/go-did/vc" @@ -128,7 +128,7 @@ func TestWrapper_handleUserLanding(t *testing.T) { sessionKey, err := jwk.ParseKey(userSession.Wallet.JWK) require.NoError(t, err) assert.NotEmpty(t, sessionKey.KeyID) - assert.Equal(t, jwa.EC, sessionKey.KeyType()) + assert.Equal(t, jwa.EC(), sessionKey.KeyType()) // check for details of issued NutsEmployeeCredential assert.Equal(t, "NutsEmployeeCredential", employeeCredentialTemplate.Type[0].String()) employeeCredentialSubject := employeeCredentialTemplate.CredentialSubject[0] diff --git a/auth/api/iam/validation.go b/auth/api/iam/validation.go index 7809a3ab4f..1af48fa405 100644 --- a/auth/api/iam/validation.go +++ b/auth/api/iam/validation.go @@ -55,7 +55,7 @@ func (r Wrapper) validatePresentationAudience(presentation vc.VerifiablePresenta var audience []string switch presentation.Format() { case vc.JWTPresentationProofFormat: - audience = presentation.JWT().Audience() + audience, _ = presentation.JWT().Audience() case vc.JSONLDPresentationProofFormat: proof, err := credential.ParseLDProof(presentation) if err != nil { diff --git a/auth/client/iam/client.go b/auth/client/iam/client.go index 0020fea550..88e4075c7d 100644 --- a/auth/client/iam/client.go +++ b/auth/client/iam/client.go @@ -24,9 +24,10 @@ import ( "encoding/json" "errors" "fmt" - "github.com/lestrrat-go/jwx/v2/jws" - "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/lestrrat-go/jwx/v3/jws" + "github.com/lestrrat-go/jwx/v3/jwt" "github.com/nuts-foundation/nuts-node/crypto" + "github.com/nuts-foundation/nuts-node/crypto/jwx" "github.com/nuts-foundation/nuts-node/vdr/resolver" "io" "net/http" @@ -279,12 +280,13 @@ func (hb HTTPClient) OpenIDConfiguration(ctx context.Context, issuerURL string) if err != nil { return nil, fmt.Errorf("unable to parse response: %w", err) } - claims, err := token.AsMap(ctx) + claims, err := jwx.ClaimsAsMap(token) if err != nil { return nil, fmt.Errorf("unable to parse response: %w", err) } // hack, broken iat - claims["iat"] = token.IssuedAt().Unix() + iat, _ := token.IssuedAt() + claims["iat"] = iat.Unix() asJSON, _ := json.Marshal(claims) if err = json.Unmarshal(asJSON, &configuration); err != nil { return nil, fmt.Errorf("unable to unmarshal response: %w", err) @@ -294,7 +296,7 @@ func (hb HTTPClient) OpenIDConfiguration(ctx context.Context, issuerURL string) func (hb HTTPClient) KeyProvider() jws.KeyProviderFunc { return func(context context.Context, keySink jws.KeySink, signature *jws.Signature, message *jws.Message) error { - keyID := signature.ProtectedHeaders().KeyID() + keyID, _ := signature.ProtectedHeaders().KeyID() publicKey, err := hb.keyResolver.ResolveKeyByID(keyID, nil, resolver.AssertionMethod) if err != nil { return fmt.Errorf("failed to resolve key (kid=%s): %w", keyID, err) diff --git a/auth/client/iam/client_test.go b/auth/client/iam/client_test.go index a63a616741..18828df60c 100644 --- a/auth/client/iam/client_test.go +++ b/auth/client/iam/client_test.go @@ -24,7 +24,8 @@ import ( "crypto/ecdsa" "encoding/json" "github.com/google/uuid" - "github.com/lestrrat-go/jwx/v2/jws" + "github.com/lestrrat-go/jwx/v3/jwk" + "github.com/lestrrat-go/jwx/v3/jws" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/nuts-node/audit" nutsCrypto "github.com/nuts-foundation/nuts-node/crypto" @@ -221,6 +222,7 @@ func TestHTTPClient_OpenIDConfiguration(t *testing.T) { ctx := context.Background() configuration := oauth.OpenIDConfiguration{ Issuer: "issuer", + JWKs: jwk.NewSet(), } // create jwt @@ -229,6 +231,9 @@ func TestHTTPClient_OpenIDConfiguration(t *testing.T) { claims := make(map[string]interface{}) asJson, _ := json.Marshal(configuration) _ = json.Unmarshal(asJson, &claims) + // jwx v3 rejects a token whose "exp" is set to the zero value (epoch) as expired; + // the marshaled zero-value OpenIDConfiguration includes "exp":0, so drop it to keep the token valid. + delete(claims, "exp") alg, _ := nutsCrypto.SignatureAlgorithm(testKey.Public()) headers := map[string]interface{}{jws.AlgorithmKey: alg, jws.KeyIDKey: "test"} token, err := nutsCrypto.SignJWT(audit.TestContext(), testKey, alg, claims, headers) @@ -276,7 +281,7 @@ func TestHTTPClient_OpenIDConfiguration(t *testing.T) { require.Error(t, err) require.Nil(t, response) - assert.EqualError(t, err, "unable to parse response: failed to parse jws: invalid byte sequence") + assert.EqualError(t, err, "unable to parse response: jwt.Parse: failed to parse token: jws.Verify: failed to parse jws: jws.Parse: failed to parse compact format: jws.Parse: invalid compact serialization format: jwsbb: invalid number of segments") }) t.Run("error - unknown key", func(t *testing.T) { otherClient := &HTTPClient{ @@ -290,7 +295,7 @@ func TestHTTPClient_OpenIDConfiguration(t *testing.T) { require.Error(t, err) require.Nil(t, response) - assert.EqualError(t, err, "unable to parse response: could not verify message using any of the signatures or keys") + assert.EqualError(t, err, "unable to parse response: jwt.Parse: failed to parse token: jws.Verify: could not verify message using any of the signatures or keys: jws.Verify: failed to verify signature #1 with key *ecdsa.PublicKey: invalid ECDSA signature\njws.Verify: signature #1: tried 1 key(s) but none verified successfully") }) } diff --git a/auth/oauth/types.go b/auth/oauth/types.go index c0a6d769d2..fd6752013f 100644 --- a/auth/oauth/types.go +++ b/auth/oauth/types.go @@ -21,7 +21,7 @@ package oauth import ( "encoding/json" - "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v3/jwk" "github.com/nuts-foundation/nuts-node/core" "net/url" ) diff --git a/auth/oauth/types_test.go b/auth/oauth/types_test.go index e0932a7991..fb528b61b4 100644 --- a/auth/oauth/types_test.go +++ b/auth/oauth/types_test.go @@ -120,7 +120,7 @@ func TestOpenIDConfiguration_UnmarshalJSON(t *testing.T) { "key_ops": ["verify"], "kid": "key1", "kty": "RSA", - "n": "pnXBOusEANuug6ewezb9J_", + "n": "wCSGKw5VuZSGYmSL8G6AVHZBNfxrdZArTSdQsDCo33vdneJhaccynjUMrh6BHpDlJIatbQlMkvDNu3bWllBmXtLx4in6z-bGzaE9XIv0Wq2jYwKyx0Jbf4XD9Qn5LMtt9G5Pbcjilemr2I_J1CwS7pfiUMqsiBMmfc0Gdih2sFkjVNrS_4ZPII2JjFGH1Sn-X0kYm8c7XBYZdXRdoTN6ABXgRXfvdbvxfmlddAVyuUoCiaMDv28hSAjznr93QShnxHO4XVefuPIiRhHCPtsppnVWY_hDyLVPLeqA-Xd3CuQCxHRnU6hPmuWHwq_bOcAs7NKrG1ubFSM60RB5jSSepQ", "use": "sig" } ] diff --git a/auth/services/irma/validator.go b/auth/services/irma/validator.go index 1ba86593a0..a3aaf8440b 100644 --- a/auth/services/irma/validator.go +++ b/auth/services/irma/validator.go @@ -26,7 +26,7 @@ import ( "strings" "time" - "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/lestrrat-go/jwx/v3/jwt" "github.com/nuts-foundation/go-did/vc" "github.com/nuts-foundation/nuts-node/auth/contract" diff --git a/auth/services/oauth/authz_server.go b/auth/services/oauth/authz_server.go index 4aa6426d44..c7d53b49f9 100644 --- a/auth/services/oauth/authz_server.go +++ b/auth/services/oauth/authz_server.go @@ -26,7 +26,7 @@ import ( "fmt" "time" - "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/lestrrat-go/jwx/v3/jwt" "github.com/nuts-foundation/go-did/did" vc2 "github.com/nuts-foundation/go-did/vc" "github.com/nuts-foundation/nuts-node/auth/contract" @@ -35,6 +35,7 @@ import ( "github.com/nuts-foundation/nuts-node/auth/services" "github.com/nuts-foundation/nuts-node/core" nutsCrypto "github.com/nuts-foundation/nuts-node/crypto" + "github.com/nuts-foundation/nuts-node/crypto/jwx" "github.com/nuts-foundation/nuts-node/didman" "github.com/nuts-foundation/nuts-node/jsonld" "github.com/nuts-foundation/nuts-node/vcr" @@ -88,9 +89,9 @@ type organizationIdentity struct { } func (c validationContext) userIdentity() (*vc2.VerifiablePresentation, error) { - claim, ok := c.jwtBearerToken.Get(userIdentityClaim) + var claim interface{} // If no credentials then OK - if !ok || claim == nil { + if err := c.jwtBearerToken.Get(userIdentityClaim, &claim); err != nil || claim == nil { return nil, nil } @@ -104,8 +105,8 @@ func (c validationContext) userIdentity() (*vc2.VerifiablePresentation, error) { } func (c validationContext) stringVal(claim string) *string { - val, ok := c.jwtBearerToken.Get(claim) - if !ok { + var val interface{} + if err := c.jwtBearerToken.Get(claim, &val); err != nil { return nil } stringVal, ok := val.(string) @@ -118,10 +119,10 @@ func (c validationContext) stringVal(claim string) *string { func (c validationContext) verifiableCredentials() ([]vc2.VerifiableCredential, error) { vcs := make([]vc2.VerifiableCredential, 0) - claim, ok := c.jwtBearerToken.Get(vcClaim) + var claim interface{} // If no credentials then OK - if !ok || claim == nil { + if err := c.jwtBearerToken.Get(vcClaim, &claim); err != nil || claim == nil { return vcs, nil } @@ -211,8 +212,8 @@ func (s *authzServer) CreateAccessToken(ctx context.Context, request services.Cr if err != nil { var requesterDID, authorizerDID string if validationCtx.jwtBearerToken != nil { - requesterDID = validationCtx.jwtBearerToken.Issuer() - authorizerDID = validationCtx.jwtBearerToken.Subject() + requesterDID, _ = validationCtx.jwtBearerToken.Issuer() + authorizerDID, _ = validationCtx.jwtBearerToken.Subject() } log.Logger(). WithField(core.LogFieldRequesterDID, requesterDID). @@ -238,7 +239,9 @@ func (s *authzServer) validateAccessTokenRequest(ctx context.Context, bearerToke } // check the maximum validity, according to RFC003 §5.2.1.4 - if validationCtx.jwtBearerToken.Expiration().Sub(validationCtx.jwtBearerToken.IssuedAt()).Seconds() > BearerTokenMaxValidity { + expiration, _ := validationCtx.jwtBearerToken.Expiration() + issuedAt, _ := validationCtx.jwtBearerToken.IssuedAt() + if expiration.Sub(issuedAt).Seconds() > BearerTokenMaxValidity { return validationCtx, errors.New("JWT validity too long") } @@ -323,18 +326,20 @@ func (s *authzServer) validatePurposeOfUse(context *validationContext) error { // check if the aud service identifier matches the oauth endpoint of the requested service func (s *authzServer) validateAudience(context *validationContext) error { - if len(context.jwtBearerToken.Audience()) != 1 { + audience, _ := context.jwtBearerToken.Audience() + if len(audience) != 1 { return errors.New("aud does not contain a single URI") } // parsing is already done in a previous check - subject, _ := did.ParseDID(context.jwtBearerToken.Subject()) + sub, _ := context.jwtBearerToken.Subject() + subject, _ := did.ParseDID(sub) endpointURL, err := s.serviceResolver.GetCompoundServiceEndpoint(*subject, context.purposeOfUse, services.OAuthEndpointType, true) if err != nil { return err } - if context.jwtBearerToken.Audience()[0] != endpointURL { + if audience[0] != endpointURL { return errors.New("aud does not contain correct endpoint URL") } return nil @@ -344,13 +349,14 @@ func (s *authzServer) validateAudience(context *validationContext) error { // - the signing key (KID) must be present as assertionMethod in the issuer's DID. // - the requester name/city which must match the login contract. func (s *authzServer) validateIssuer(vContext *validationContext) error { - if requester, err := did.ParseDID(vContext.jwtBearerToken.Issuer()); err != nil { + iss, _ := vContext.jwtBearerToken.Issuer() + if requester, err := did.ParseDID(iss); err != nil { return fmt.Errorf(errInvalidIssuerFmt, err) } else { vContext.requester = requester } - validationTime := vContext.jwtBearerToken.IssuedAt() + validationTime, _ := vContext.jwtBearerToken.IssuedAt() metadata := &resolver.ResolveMetadata{ ResolveTime: &validationTime, } @@ -359,7 +365,7 @@ func (s *authzServer) validateIssuer(vContext *validationContext) error { } searchTerms := []vcr.SearchTerm{ - {IRIPath: jsonld.CredentialSubjectPath, Value: vContext.jwtBearerToken.Issuer()}, + {IRIPath: jsonld.CredentialSubjectPath, Value: iss}, {IRIPath: jsonld.OrganizationNamePath, Type: vcr.NotNil}, {IRIPath: jsonld.OrganizationCityPath, Type: vcr.NotNil}, } @@ -395,17 +401,18 @@ func (s *authzServer) validateIssuer(vContext *validationContext) error { // check if the authorizer is registered by this vendor, according to RFC003 §5.2.1.8 func (s *authzServer) validateSubject(ctx context.Context, validationCtx *validationContext) error { - if validationCtx.jwtBearerToken.Subject() == "" { + sub, _ := validationCtx.jwtBearerToken.Subject() + if sub == "" { return fmt.Errorf(errInvalidSubjectFmt, errors.New("missing")) } - subject, err := did.ParseDID(validationCtx.jwtBearerToken.Subject()) + subject, err := did.ParseDID(sub) if err != nil { return fmt.Errorf(errInvalidSubjectFmt, err) } validationCtx.authorizer = subject - iat := validationCtx.jwtBearerToken.IssuedAt() + iat, _ := validationCtx.jwtBearerToken.IssuedAt() signingKeyID, _, err := s.keyResolver.ResolveKey(*subject, &iat, resolver.NutsSigningKeyType) if err != nil { return err @@ -446,9 +453,9 @@ func (s *authzServer) validateAuthorizationCredentials(context *validationContex return nil } - iat := context.jwtBearerToken.IssuedAt() - iss := context.jwtBearerToken.Issuer() - sub := context.jwtBearerToken.Subject() + iat, _ := context.jwtBearerToken.IssuedAt() + iss, _ := context.jwtBearerToken.Issuer() + sub, _ := context.jwtBearerToken.Subject() for _, authCred := range vcs { // first check if the VC is valid and if the signature is correct @@ -493,7 +500,19 @@ func (s *authzServer) parseAndValidateJwtBearerToken(context *validationContext) // IntrospectAccessToken fills the fields in NutsAccessToken from the given Jwt Access Token func (s *authzServer) IntrospectAccessToken(ctx context.Context, accessToken string) (*services.NutsAccessToken, error) { + // Validate typ header before full parsing to reject non-access-token JWTs early + headers, err := nutsCrypto.ExtractProtectedHeaders(accessToken) + if err != nil { + return nil, fmt.Errorf("invalid access token headers: %w", err) + } + typ, _ := headers["typ"].(string) + if typ != "at+jwt" { + return nil, fmt.Errorf("invalid access token typ header (expected 'at+jwt', got '%s')", typ) + } + + var kidHdr string token, err := nutsCrypto.ParseJWT(accessToken, func(kid string) (crypto.PublicKey, error) { + kidHdr = kid exists, err := s.privateKeyStore.Exists(ctx, kid) if err != nil { return nil, fmt.Errorf("could not check if JWT signing key exists: %w", err) @@ -509,16 +528,47 @@ func (s *authzServer) IntrospectAccessToken(ctx context.Context, accessToken str result := &services.NutsAccessToken{} - if err := result.FromMap(token.PrivateClaims()); err != nil { + // Extract all claims, then drop the registered ones to mirror jwx v2's token.PrivateClaims. + // jwx.ClaimsAsMap preserves null-valued claims (a per-claim Get loop errors on null values in v3). + privateClaims, err := jwx.ClaimsAsMap(token) + if err != nil { return nil, err } + for _, k := range []string{jwt.IssuerKey, jwt.SubjectKey, jwt.AudienceKey, jwt.ExpirationKey, jwt.NotBeforeKey, jwt.IssuedAtKey, jwt.JwtIDKey} { + delete(privateClaims, k) + } + if err := result.FromMap(privateClaims); err != nil { + return nil, err + } + + result.Subject, _ = token.Subject() + result.Issuer, _ = token.Issuer() + iat, _ := token.IssuedAt() + result.IssuedAt = iat.Unix() + exp, _ := token.Expiration() + result.Expiration = exp.Unix() - result.Subject = token.Subject() - result.Issuer = token.Issuer() - result.IssuedAt = token.IssuedAt().Unix() - result.Expiration = token.Expiration().Unix() + // Validate required claims + if result.Issuer == "" { + return nil, errors.New("missing required 'iss' claim in access token") + } + if result.Subject == "" { + return nil, errors.New("missing required 'sub' claim in access token") + } + if result.Service == "" { + return nil, errors.New("missing required 'service' claim in access token") + } + + // Validate issuer-to-kid binding: the DID in the kid header must match the iss claim + kidDID, err := resolver.GetDIDFromURL(kidHdr) + if err != nil { + return nil, fmt.Errorf("invalid kid header in access token: %w", err) + } + if kidDID.String() != result.Issuer { + return nil, fmt.Errorf("access token issuer (%s) does not match signing key DID (%s)", result.Issuer, kidDID.String()) + } - return result, err + return result, nil } // todo split this func for easier testing @@ -566,7 +616,7 @@ func (s *authzServer) buildAccessToken(ctx context.Context, requester did.DID, a if err != nil { return "", accessToken, err } - token, err := s.privateKeyStore.SignJWT(ctx, keyVals, nil, signingKeyID) + token, err := s.privateKeyStore.SignJWT(ctx, keyVals, map[string]interface{}{"typ": "at+jwt"}, signingKeyID) if err != nil { return token, accessToken, fmt.Errorf("could not build accessToken: %w", err) } diff --git a/auth/services/oauth/authz_server_test.go b/auth/services/oauth/authz_server_test.go index bb0cbc0771..23f266f70b 100644 --- a/auth/services/oauth/authz_server_test.go +++ b/auth/services/oauth/authz_server_test.go @@ -32,9 +32,9 @@ import ( "testing" "time" - "github.com/lestrrat-go/jwx/v2/jwa" - "github.com/lestrrat-go/jwx/v2/jws" - "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/lestrrat-go/jwx/v3/jwa" + "github.com/lestrrat-go/jwx/v3/jws" + "github.com/lestrrat-go/jwx/v3/jwt" ssi "github.com/nuts-foundation/go-did" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/go-did/vc" @@ -193,7 +193,7 @@ func TestAuth_CreateAccessToken(t *testing.T) { t.Run("return internal errors when secureMode=false", func(t *testing.T) { ctx := setup(createContext(t)) ctx.oauthService.secureMode = false - ctx.keyStore.EXPECT().SignJWT(ctx.audit, gomock.Any(), nil, gomock.Any()).Return("", errors.New("signing error")) + ctx.keyStore.EXPECT().SignJWT(ctx.audit, gomock.Any(), gomock.Any(), gomock.Any()).Return("", errors.New("signing error")) tokenCtx := validContext(t) signToken(tokenCtx) @@ -206,7 +206,7 @@ func TestAuth_CreateAccessToken(t *testing.T) { t.Run("mask internal errors when secureMode=true", func(t *testing.T) { ctx := setup(createContext(t)) ctx.oauthService.secureMode = true - ctx.keyStore.EXPECT().SignJWT(gomock.Any(), gomock.Any(), nil, gomock.Any()).Return("", errors.New("signing error")) + ctx.keyStore.EXPECT().SignJWT(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return("", errors.New("signing error")) tokenCtx := validContext(t) signToken(tokenCtx) @@ -227,7 +227,7 @@ func TestAuth_CreateAccessToken(t *testing.T) { testCtx.didResolver.EXPECT().Resolve(authorizerDID, gomock.Any()).Return(getAuthorizerDIDDocument(), nil, nil).AnyTimes() testCtx.serviceResolver.EXPECT().GetCompoundServiceEndpoint(authorizerDID, expectedService, services.OAuthEndpointType, true).Return(expectedAudience, nil) testCtx.keyStore.EXPECT().Exists(testCtx.audit, authorizerSigningKeyID).Return(true, nil) - testCtx.keyStore.EXPECT().SignJWT(gomock.Any(), gomock.Any(), nil, authorizerSigningKeyID).Return("expectedAccessToken", nil) + testCtx.keyStore.EXPECT().SignJWT(gomock.Any(), gomock.Any(), gomock.Any(), authorizerSigningKeyID).Return("expectedAccessToken", nil) testCtx.verifier.EXPECT().Verify(gomock.Any(), true, true, gomock.Any()).Return(nil) ctx := validContext(t) @@ -249,7 +249,7 @@ func TestAuth_CreateAccessToken(t *testing.T) { ctx.didResolver.EXPECT().Resolve(authorizerDID, gomock.Any()).Return(getAuthorizerDIDDocument(), nil, nil).AnyTimes() ctx.serviceResolver.EXPECT().GetCompoundServiceEndpoint(authorizerDID, expectedService, services.OAuthEndpointType, true).Return(expectedAudience, nil) ctx.keyStore.EXPECT().Exists(ctx.audit, authorizerSigningKeyID).Return(true, nil) - ctx.keyStore.EXPECT().SignJWT(gomock.Any(), gomock.Any(), nil, authorizerSigningKeyID).Return("expectedAT", nil) + ctx.keyStore.EXPECT().SignJWT(gomock.Any(), gomock.Any(), gomock.Any(), authorizerSigningKeyID).Return("expectedAT", nil) ctx.contractNotary.EXPECT().VerifyVP(gomock.Any(), nil).Return(services.TestVPVerificationResult{ Val: contract.Valid, DAttributes: map[string]string{"name": "Henk de Vries"}, @@ -517,7 +517,7 @@ func TestService_validateAuthorizationCredentials(t *testing.T) { err := ctx.oauthService.validateAuthorizationCredentials(tokenCtx) - assert.EqualError(t, err, "invalid jwt.vcs: cannot unmarshal authorization credential: invalid JWT") + assert.EqualError(t, err, "invalid jwt.vcs: cannot unmarshal authorization credential: jwt.Parse: failed to parse token: unknown payload type (payload is not JWT?)") }) t.Run("error - jwt.iss <> credentialSubject.ID mismatch", func(t *testing.T) { @@ -563,14 +563,14 @@ func TestService_parseAndValidateJwtBearerToken(t *testing.T) { } err := ctx.oauthService.parseAndValidateJwtBearerToken(tokenCtx) assert.Nil(t, tokenCtx.jwtBearerToken) - assert.Equal(t, "invalid compact serialization format: invalid number of segments", err.Error()) + assert.Equal(t, "jws.ParseString: failed to parse string: jws.Parse: failed to parse compact format: jws.Parse: invalid compact serialization format: jwsbb: invalid number of segments", err.Error()) tokenCtx2 := &validationContext{ rawJwtBearerToken: "123.456.787", } err = ctx.oauthService.parseAndValidateJwtBearerToken(tokenCtx2) assert.Nil(t, tokenCtx.jwtBearerToken) - assert.Equal(t, "failed to parse JOSE headers: invalid character '×' looking for beginning of value", err.Error()) + assert.Equal(t, "jws.ParseString: failed to parse string: jws.Parse: failed to parse compact format: failed to parse JOSE headers: invalid character '×' looking for beginning of value", err.Error()) }) t.Run("wrong signing algorithm", func(t *testing.T) { @@ -586,7 +586,7 @@ func TestService_parseAndValidateJwtBearerToken(t *testing.T) { token := jwt.New() hdrs := jws.NewHeaders() hdrs.Set(jws.KeyIDKey, keyID) - signedToken, err := jwt.Sign(token, jwt.WithKey(jwa.RS256, privateKey, jws.WithProtectedHeaders(hdrs))) + signedToken, err := jwt.Sign(token, jwt.WithKey(jwa.RS256(), privateKey, jws.WithProtectedHeaders(hdrs))) require.NoError(t, err) tokenCtx := &validationContext{ @@ -605,7 +605,8 @@ func TestService_parseAndValidateJwtBearerToken(t *testing.T) { err := ctx.oauthService.parseAndValidateJwtBearerToken(tokenCtx) assert.NoError(t, err) - assert.Equal(t, requesterDID.String(), tokenCtx.jwtBearerToken.Issuer()) + iss, _ := tokenCtx.jwtBearerToken.Issuer() + assert.Equal(t, requesterDID.String(), iss) assert.Equal(t, requesterSigningKeyID, tokenCtx.kid) }) @@ -636,7 +637,7 @@ func TestService_parseAndValidateJwtBearerToken(t *testing.T) { ctx.keyResolver.EXPECT().ResolveKeyByID(requesterSigningKeyID, nil, resolver.NutsSigningKeyType).Return(requesterSigningKey.PublicKey, nil) err := ctx.oauthService.parseAndValidateJwtBearerToken(tokenCtx) - assert.EqualError(t, err, "\"exp\" not satisfied") + assert.EqualError(t, err, "jwt.ParseString: failed to parse string: jwt.Validate: validation failed: \"exp\" not satisfied: token is expired") }) } @@ -649,7 +650,7 @@ func TestService_buildAccessToken(t *testing.T) { ctx.keyResolver.EXPECT().ResolveKey(authorizerDID, gomock.Any(), resolver.NutsSigningKeyType).MinTimes(1).Return(authorizerSigningKeyID, authorizerSigningKey, nil) var actualClaims map[string]interface{} - ctx.keyStore.EXPECT().SignJWT(gomock.Any(), gomock.Any(), nil, gomock.Any()). + ctx.keyStore.EXPECT().SignJWT(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). DoAndReturn(func(_ context.Context, inputClaims map[string]interface{}, headers map[string]interface{}, kid string) (token string, err error) { actualClaims = inputClaims return "expectedAT", nil @@ -686,52 +687,55 @@ func TestService_IntrospectAccessToken(t *testing.T) { t.Run("validate access token", func(t *testing.T) { ctx := createContext(t) - ctx.keyResolver.EXPECT().ResolveKeyByID(requesterSigningKeyID, nil, resolver.NutsSigningKeyType).MinTimes(1).Return(requesterSigningKey.Public(), nil) - ctx.keyStore.EXPECT().Exists(ctx.audit, requesterSigningKeyID).Return(true, nil) + ctx.keyResolver.EXPECT().ResolveKeyByID(authorizerSigningKeyID, nil, resolver.NutsSigningKeyType).MinTimes(1).Return(authorizerSigningKey.Public(), nil) + ctx.keyStore.EXPECT().Exists(ctx.audit, authorizerSigningKeyID).Return(true, nil) - // First build an access token + // First build an access token (signed by authorizer's key, matching iss) tokenCtx := validAccessToken() - signToken(tokenCtx) + signAccessToken(tokenCtx) // Then validate it claims, err := ctx.oauthService.IntrospectAccessToken(ctx.audit, tokenCtx.rawJwtBearerToken) require.NoError(t, err) require.NotNil(t, claims) - assert.Equal(t, tokenCtx.jwtBearerToken.Subject(), claims.Subject) - assert.Equal(t, tokenCtx.jwtBearerToken.Issuer(), claims.Issuer) - assert.Equal(t, tokenCtx.jwtBearerToken.IssuedAt().Unix(), claims.IssuedAt) - assert.Equal(t, tokenCtx.jwtBearerToken.Expiration().Unix(), claims.Expiration) + subject, _ := tokenCtx.jwtBearerToken.Subject() + issuer, _ := tokenCtx.jwtBearerToken.Issuer() + issuedAt, _ := tokenCtx.jwtBearerToken.IssuedAt() + expiration, _ := tokenCtx.jwtBearerToken.Expiration() + assert.Equal(t, subject, claims.Subject) + assert.Equal(t, issuer, claims.Issuer) + assert.Equal(t, issuedAt.Unix(), claims.IssuedAt) + assert.Equal(t, expiration.Unix(), claims.Expiration) + assert.Equal(t, expectedService, claims.Service) }) t.Run("invalid signature", func(t *testing.T) { ctx := createContext(t) - ctx.keyResolver.EXPECT().ResolveKeyByID(requesterSigningKeyID, nil, resolver.NutsSigningKeyType).MinTimes(1).Return(requesterSigningKey.Public(), nil) - ctx.keyStore.EXPECT().Exists(ctx.audit, requesterSigningKeyID).Return(true, nil) + ctx.keyResolver.EXPECT().ResolveKeyByID(authorizerSigningKeyID, nil, resolver.NutsSigningKeyType).MinTimes(1).Return(authorizerSigningKey.Public(), nil) + ctx.keyStore.EXPECT().Exists(ctx.audit, authorizerSigningKeyID).Return(true, nil) - // First build an access token + // Build a token with correct claims and kid, but sign with a different key tokenCtx := validAccessToken() - signingKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - signTokenWithKey(tokenCtx, signingKey) + wrongKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + signTokenWithKeyAndHeaders(tokenCtx, wrongKey, authorizerSigningKeyID, map[string]interface{}{"typ": "at+jwt"}) - // Then validate it + // Signature verification should fail claims, err := ctx.oauthService.IntrospectAccessToken(ctx.audit, tokenCtx.rawJwtBearerToken) - require.EqualError(t, err, "could not verify message using any of the signatures or keys") + require.EqualError(t, err, "jwt.ParseString: failed to parse string: signature verification failed for ES256: invalid ECDSA signature") require.Nil(t, claims) }) t.Run("private key not present", func(t *testing.T) { ctx := createContext(t) - ctx.keyStore.EXPECT().Exists(ctx.audit, requesterSigningKeyID).Return(false, nil) + ctx.keyStore.EXPECT().Exists(ctx.audit, authorizerSigningKeyID).Return(false, nil) - // First build an access token - tokenCtx := validContext(t) - signToken(tokenCtx) + tokenCtx := validAccessToken() + signAccessToken(tokenCtx) - // Then validate it _, err := ctx.oauthService.IntrospectAccessToken(ctx.audit, tokenCtx.rawJwtBearerToken) assert.Error(t, err) }) @@ -739,17 +743,127 @@ func TestService_IntrospectAccessToken(t *testing.T) { t.Run("key not present on DID", func(t *testing.T) { ctx := createContext(t) - ctx.keyStore.EXPECT().Exists(ctx.audit, requesterSigningKeyID).Return(true, nil) - ctx.keyResolver.EXPECT().ResolveKeyByID(requesterSigningKeyID, nil, resolver.NutsSigningKeyType).MinTimes(1).Return(nil, resolver.ErrNotFound) + ctx.keyStore.EXPECT().Exists(ctx.audit, authorizerSigningKeyID).Return(true, nil) + ctx.keyResolver.EXPECT().ResolveKeyByID(authorizerSigningKeyID, nil, resolver.NutsSigningKeyType).MinTimes(1).Return(nil, resolver.ErrNotFound) - // First build an access token - tokenCtx := validContext(t) - signToken(tokenCtx) + tokenCtx := validAccessToken() + signAccessToken(tokenCtx) - // Then validate it _, err := ctx.oauthService.IntrospectAccessToken(ctx.audit, tokenCtx.rawJwtBearerToken) assert.Error(t, err) }) + + t.Run("rejects JWT with wrong typ header", func(t *testing.T) { + ctx := createContext(t) + + // JWT library defaults typ to "JWT" when not explicitly set. + // Both explicit typ:"JWT" and default typ:"JWT" must be rejected. + tokenCtx := validAccessToken() + signTokenWithKeyAndHeaders(tokenCtx, authorizerSigningKey, authorizerSigningKeyID, map[string]interface{}{"typ": "JWT"}) + + _, err := ctx.oauthService.IntrospectAccessToken(ctx.audit, tokenCtx.rawJwtBearerToken) + assert.EqualError(t, err, "invalid access token typ header (expected 'at+jwt', got 'JWT')") + }) + + t.Run("rejects VP JWT replayed as access token", func(t *testing.T) { + ctx := createContext(t) + + // Construct a VP JWT: typ=JWT, sub set, vp claim present, no iss or service + vpClaims := map[string]interface{}{ + jwt.SubjectKey: requesterDID.String(), + jwt.JwtIDKey: requesterDID.String() + "#vp-id", + jwt.NotBeforeKey: time.Now().Unix(), + jwt.ExpirationKey: time.Now().Add(5 * time.Minute).Unix(), + "vp": map[string]interface{}{ + "@context": []string{"https://www.w3.org/2018/credentials/v1"}, + "type": []string{"VerifiablePresentation"}, + "verifiableCredential": []interface{}{}, + }, + } + token := jwt.New() + for k, v := range vpClaims { + _ = token.Set(k, v) + } + tokenCtx := &validationContext{jwtBearerToken: token, kid: requesterSigningKeyID} + signTokenWithKeyAndHeaders(tokenCtx, requesterSigningKey, requesterSigningKeyID, map[string]interface{}{"typ": "JWT"}) + + _, err := ctx.oauthService.IntrospectAccessToken(ctx.audit, tokenCtx.rawJwtBearerToken) + assert.EqualError(t, err, "invalid access token typ header (expected 'at+jwt', got 'JWT')") + }) + + t.Run("rejects token with missing service claim", func(t *testing.T) { + ctx := createContext(t) + + ctx.keyResolver.EXPECT().ResolveKeyByID(requesterSigningKeyID, nil, resolver.NutsSigningKeyType).MinTimes(1).Return(requesterSigningKey.Public(), nil) + ctx.keyStore.EXPECT().Exists(ctx.audit, requesterSigningKeyID).Return(true, nil) + + // Valid AT structure but without the service claim + claims := map[string]interface{}{ + jwt.ExpirationKey: time.Now().Add(5 * time.Second).Unix(), + jwt.IssuedAtKey: time.Now().UTC(), + jwt.SubjectKey: requesterDID.String(), + jwt.IssuerKey: authorizerDID.String(), + } + token := jwt.New() + for k, v := range claims { + _ = token.Set(k, v) + } + tokenCtx := &validationContext{jwtBearerToken: token, kid: requesterSigningKeyID} + signTokenWithKeyAndHeaders(tokenCtx, requesterSigningKey, requesterSigningKeyID, map[string]interface{}{"typ": "at+jwt"}) + + _, err := ctx.oauthService.IntrospectAccessToken(ctx.audit, tokenCtx.rawJwtBearerToken) + assert.EqualError(t, err, "missing required 'service' claim in access token") + }) + + t.Run("rejects token with missing iss claim", func(t *testing.T) { + ctx := createContext(t) + + ctx.keyResolver.EXPECT().ResolveKeyByID(requesterSigningKeyID, nil, resolver.NutsSigningKeyType).MinTimes(1).Return(requesterSigningKey.Public(), nil) + ctx.keyStore.EXPECT().Exists(ctx.audit, requesterSigningKeyID).Return(true, nil) + + // AT with service but no issuer + claims := map[string]interface{}{ + jwt.ExpirationKey: time.Now().Add(5 * time.Second).Unix(), + jwt.IssuedAtKey: time.Now().UTC(), + jwt.SubjectKey: requesterDID.String(), + "service": expectedService, + } + token := jwt.New() + for k, v := range claims { + _ = token.Set(k, v) + } + tokenCtx := &validationContext{jwtBearerToken: token, kid: requesterSigningKeyID} + signTokenWithKeyAndHeaders(tokenCtx, requesterSigningKey, requesterSigningKeyID, map[string]interface{}{"typ": "at+jwt"}) + + _, err := ctx.oauthService.IntrospectAccessToken(ctx.audit, tokenCtx.rawJwtBearerToken) + assert.EqualError(t, err, "missing required 'iss' claim in access token") + }) + + t.Run("rejects token with iss/kid DID mismatch", func(t *testing.T) { + ctx := createContext(t) + + ctx.keyResolver.EXPECT().ResolveKeyByID(requesterSigningKeyID, nil, resolver.NutsSigningKeyType).MinTimes(1).Return(requesterSigningKey.Public(), nil) + ctx.keyStore.EXPECT().Exists(ctx.audit, requesterSigningKeyID).Return(true, nil) + + // iss is authorizerDID but kid belongs to requesterDID — DID mismatch + claims := map[string]interface{}{ + jwt.ExpirationKey: time.Now().Add(5 * time.Second).Unix(), + jwt.IssuedAtKey: time.Now().UTC(), + jwt.SubjectKey: requesterDID.String(), + jwt.IssuerKey: authorizerDID.String(), + "service": expectedService, + } + token := jwt.New() + for k, v := range claims { + _ = token.Set(k, v) + } + tokenCtx := &validationContext{jwtBearerToken: token, kid: requesterSigningKeyID} + signTokenWithKeyAndHeaders(tokenCtx, requesterSigningKey, requesterSigningKeyID, map[string]interface{}{"typ": "at+jwt"}) + + _, err := ctx.oauthService.IntrospectAccessToken(ctx.audit, tokenCtx.rawJwtBearerToken) + assert.ErrorContains(t, err, "access token issuer") + assert.ErrorContains(t, err, "does not match signing key DID") + }) } func TestAuth_Configure(t *testing.T) { @@ -809,6 +923,7 @@ func validAccessToken() *validationContext { jwt.NotBeforeKey: 0, jwt.IssuerKey: authorizerDID.String(), userIdentityClaim: usi, + "service": expectedService, vcClaim: []string{"credential"}, } token := jwt.New() @@ -825,16 +940,28 @@ func validAccessToken() *validationContext { } func signToken(context *validationContext) { - signTokenWithKey(context, requesterSigningKey) + signTokenWithKeyAndHeaders(context, requesterSigningKey, requesterSigningKeyID, nil) +} + +func signAccessToken(context *validationContext) { + signTokenWithKeyAndHeaders(context, authorizerSigningKey, authorizerSigningKeyID, map[string]interface{}{"typ": "at+jwt"}) } func signTokenWithKey(context *validationContext, key *ecdsa.PrivateKey) { + signTokenWithKeyAndHeaders(context, key, requesterSigningKeyID, nil) +} + +func signTokenWithKeyAndHeaders(context *validationContext, key *ecdsa.PrivateKey, kid string, extraHeaders map[string]interface{}) { hdrs := jws.NewHeaders() - err := hdrs.Set(jws.KeyIDKey, requesterSigningKeyID) - if err != nil { + if err := hdrs.Set(jws.KeyIDKey, kid); err != nil { panic(err) } - signedToken, err := jwt.Sign(context.jwtBearerToken, jwt.WithKey(jwa.ES256, key, jws.WithProtectedHeaders(hdrs))) + for k, v := range extraHeaders { + if err := hdrs.Set(k, v); err != nil { + panic(err) + } + } + signedToken, err := jwt.Sign(context.jwtBearerToken, jwt.WithKey(jwa.ES256(), key, jws.WithProtectedHeaders(hdrs))) if err != nil { panic(err) } diff --git a/auth/services/oauth/relying_party.go b/auth/services/oauth/relying_party.go index e78160a86c..c3091d19c3 100644 --- a/auth/services/oauth/relying_party.go +++ b/auth/services/oauth/relying_party.go @@ -27,7 +27,7 @@ import ( "strings" "time" - "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/lestrrat-go/jwx/v3/jwt" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/nuts-node/auth/api/auth/v1/client" "github.com/nuts-foundation/nuts-node/auth/oauth" diff --git a/core/echo_errors.go b/core/echo_errors.go index 4848ab2076..39dde1ff3c 100644 --- a/core/echo_errors.go +++ b/core/echo_errors.go @@ -50,6 +50,15 @@ const unmappedStatusCode = 0 // CreateHTTPErrorHandler returns an Echo HTTPErrorHandler that logs the error with extra fields and returns it as an HTTP response. func CreateHTTPErrorHandler() echo.HTTPErrorHandler { return func(err error, ctx echo.Context) { + // Echo can invoke the error handler twice for the same request: middleware + // like BodyDump calls c.Error(err) on its way out and still returns the err, + // causing echo's server loop to invoke the error handler a second time. + // Skip the second invocation — the response has already been written and + // the error logged by the first invocation. echo.DefaultHTTPErrorHandler + // behaves the same way. + if ctx.Response().Committed { + return + } // HTTPErrors occur e.g. when a parameter bind fails. We map this to a httpStatusCodeError so its status code // and message get directly mapped to a problem. if echoErr, ok := err.(*echo.HTTPError); ok { @@ -76,19 +85,12 @@ func CreateHTTPErrorHandler() echo.HTTPErrorHandler { } else { logMsg.Warn(title) } - if !ctx.Response().Committed { - errorWriter, _ := ctx.Get(ErrorWriterContextKey).(ErrorWriter) - if errorWriter == nil { - errorWriter = &problemErrorWriter{} - } - writeError := errorWriter.Write(ctx, statusCode, title, err) - if writeError != nil { - logger.Error(err) - } - } else { - logger. - WithError(err). - Warn("Unable to send error back to client, response already committed") + errorWriter, _ := ctx.Get(ErrorWriterContextKey).(ErrorWriter) + if errorWriter == nil { + errorWriter = &problemErrorWriter{} + } + if writeError := errorWriter.Write(ctx, statusCode, title, err); writeError != nil { + logger.Error(err) } } } @@ -191,7 +193,7 @@ func ResolveStatusCode(err error, mapping map[error]int) int { // - from handler // - if none of the above criteria match, HTTP 500 Internal Server Error is returned. func GetHTTPStatusCode(err error, ctx echo.Context) int { - if predefined, ok := err.(HTTPStatusCodeError); ok { + if predefined, ok := errors.AsType[HTTPStatusCodeError](err); ok { return predefined.StatusCode() } if predefined, ok := err.(*echo.HTTPError); ok { diff --git a/core/echo_errors_test.go b/core/echo_errors_test.go index baedee9b1f..b50e12291a 100644 --- a/core/echo_errors_test.go +++ b/core/echo_errors_test.go @@ -21,6 +21,9 @@ package core import ( "errors" "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + "github.com/sirupsen/logrus" + logrustest "github.com/sirupsen/logrus/hooks/test" "github.com/stretchr/testify/assert" "io" "net/http" @@ -89,6 +92,47 @@ func TestHttpErrorHandler(t *testing.T) { }) } +func TestHttpErrorHandler_idempotent_with_BodyDump(t *testing.T) { + // Echo's BodyDump middleware (and any other middleware that calls c.Error + // on the way out and still returns the error) causes echo to invoke the + // HTTPErrorHandler twice for the same request: once via c.Error and once + // via the err returned to echo's server loop. The second invocation must + // be a no-op so it does not log "response already committed" warnings. + logger, hook := logrustest.NewNullLogger() + logger.SetLevel(logrus.DebugLevel) + prevLogger := logrus.StandardLogger() + logrus.SetOutput(io.Discard) + prevHooks := prevLogger.Hooks + prevLogger.Hooks = make(logrus.LevelHooks) + prevLogger.AddHook(hook) + t.Cleanup(func() { + prevLogger.Hooks = prevHooks + logrus.SetOutput(prevLogger.Out) + }) + + es := echo.New() + es.HTTPErrorHandler = CreateHTTPErrorHandler() + es.Use(middleware.BodyDump(func(c echo.Context, _, _ []byte) {})) + es.Add(http.MethodGet, "/", func(c echo.Context) error { + c.Set(OperationIDContextKey, "test") + return errors.New("upstream failed") + }) + server := httptest.NewServer(es) + t.Cleanup(server.Close) + + resp, err := http.Get(server.URL + "/") + assert.NoError(t, err) + assert.Equal(t, http.StatusInternalServerError, resp.StatusCode) + body, _ := io.ReadAll(resp.Body) + assert.Contains(t, string(body), "upstream failed", + "the error must be written to the response exactly once") + + for _, entry := range hook.AllEntries() { + assert.NotContains(t, entry.Message, "response already committed", + "HTTPErrorHandler must be idempotent when invoked on a committed response") + } +} + func Test_NotFoundError(t *testing.T) { err := NotFoundError("failed: %s", "oops").(httpStatusCodeError) assert.EqualError(t, err, "failed: oops") diff --git a/crypto/api/v1/api.go b/crypto/api/v1/api.go index 99b0706b8a..49dcc18884 100644 --- a/crypto/api/v1/api.go +++ b/crypto/api/v1/api.go @@ -28,7 +28,7 @@ import ( "time" "github.com/labstack/echo/v4" - "github.com/lestrrat-go/jwx/v2/jws" + "github.com/lestrrat-go/jwx/v3/jws" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/nuts-node/audit" diff --git a/crypto/dpop/dpop.go b/crypto/dpop/dpop.go index 3f8ad8b932..fb4e422419 100644 --- a/crypto/dpop/dpop.go +++ b/crypto/dpop/dpop.go @@ -33,10 +33,10 @@ import ( "strings" "time" - "github.com/lestrrat-go/jwx/v2/jwa" - "github.com/lestrrat-go/jwx/v2/jwk" - "github.com/lestrrat-go/jwx/v2/jws" - "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/lestrrat-go/jwx/v3/jwa" + "github.com/lestrrat-go/jwx/v3/jwk" + "github.com/lestrrat-go/jwx/v3/jws" + "github.com/lestrrat-go/jwx/v3/jwt" "github.com/nuts-foundation/nuts-node/crypto/hash" "github.com/nuts-foundation/nuts-node/crypto/jwx" ) @@ -99,7 +99,7 @@ func (t *DPoP) Sign(kid string, key crypto.Signer, alg jwa.SignatureAlgorithm) ( if t.raw != "" { return "", errors.New("already signed") } - publicKeyJWK, err := jwk.FromRaw(key.Public()) + publicKeyJWK, err := jwk.Import(key.Public()) if err != nil { return "", err } @@ -137,53 +137,58 @@ func Parse(s string) (*DPoP, error) { return nil, fmt.Errorf("%w: invalid number of signatures", ErrInvalidDPoP) } headers := message.Signatures()[0].ProtectedHeaders() - if !slices.Contains(jwx.SupportedAlgorithms, headers.Algorithm()) { - return nil, fmt.Errorf("%w: invalid alg: %s", ErrInvalidDPoP, headers.Algorithm()) + alg, _ := headers.Algorithm() + if !slices.Contains(jwx.SupportedAlgorithms, alg) { + return nil, fmt.Errorf("%w: invalid alg: %s", ErrInvalidDPoP, alg) } - if headers.Type() != "dpop+jwt" { - return nil, fmt.Errorf("%w: invalid type: %s", ErrInvalidDPoP, headers.Type()) + if typ, _ := headers.Type(); typ != "dpop+jwt" { + return nil, fmt.Errorf("%w: invalid type: %s", ErrInvalidDPoP, typ) } - if headers.JWK() == nil { + headerJWK, ok := headers.JWK() + if !ok || headerJWK == nil { return nil, fmt.Errorf("%w: missing jwk header", ErrInvalidDPoP) } - if jwkIsPrivateKey(headers.JWK()) { + if jwkIsPrivateKey(headerJWK) { return nil, fmt.Errorf("%w: invalid jwk header", ErrInvalidDPoP) } - token, err := jwt.ParseString(s, jwt.WithKey(headers.Algorithm(), headers.JWK())) + token, err := jwt.ParseString(s, jwt.WithKey(alg, headerJWK)) if err != nil { return nil, errors.Join(ErrInvalidDPoP, err) } - if token.IssuedAt().IsZero() { + if iat, ok := token.IssuedAt(); !ok || iat.IsZero() { return nil, fmt.Errorf("%w: missing iat claim", ErrInvalidDPoP) } - if v, ok := token.Get(HTUKey); !ok || v == "" { + var htu string + if err := token.Get(HTUKey, &htu); err != nil || htu == "" { return nil, fmt.Errorf("%w: missing htu claim", ErrInvalidDPoP) } - if v, ok := token.Get(HTMKey); !ok || v == "" { + var htm string + if err := token.Get(HTMKey, &htm); err != nil || htm == "" { return nil, fmt.Errorf("%w: missing htm claim", ErrInvalidDPoP) } - if token.JwtID() == "" { + jti, _ := token.JwtID() + if jti == "" { return nil, fmt.Errorf("%w: missing jti claim", ErrInvalidDPoP) } - if len(token.JwtID()) > maxJtiLength { + if len(jti) > maxJtiLength { return nil, fmt.Errorf("%w: jti claim too long", ErrInvalidDPoP) } return &DPoP{raw: s, Token: token, Headers: headers}, nil } -func jwkIsPrivateKey(jwk jwk.Key) bool { +func jwkIsPrivateKey(key jwk.Key) bool { // we try to parse it as different private keys, if there's no error, it's a private key var rsaPrivateKey rsa.PrivateKey - if err := jwk.Raw(&rsaPrivateKey); err == nil { + if err := jwk.Export(key, &rsaPrivateKey); err == nil { return true } var ecPrivateKey ecdsa.PrivateKey - if err := jwk.Raw(&ecPrivateKey); err == nil { + if err := jwk.Export(key, &ecPrivateKey); err == nil { return true } var edPrivateKey ed25519.PrivateKey - if err := jwk.Raw(&edPrivateKey); err == nil { + if err := jwk.Export(key, &edPrivateKey); err == nil { return true } return false @@ -191,16 +196,18 @@ func jwkIsPrivateKey(jwk jwk.Key) bool { // HTU returns the htu claim of the DPoP token func (t DPoP) HTU() string { - if v, ok := t.Token.Get(HTUKey); ok { - return v.(string) + var htu string + if err := t.Token.Get(HTUKey, &htu); err == nil { + return htu } return "" } // HTM returns the htm claim of the DPoP token func (t DPoP) HTM() string { - if v, ok := t.Token.Get(HTMKey); ok { - return v.(string) + var htm string + if err := t.Token.Get(HTMKey, &htm); err == nil { + return htm } return "" } @@ -209,7 +216,11 @@ func (t DPoP) HTM() string { // for the url, the port is stripped. // If there is a mismatch, the reason is returned in an error. func (t DPoP) Match(jkt string, method string, url string) (bool, error) { - tp, _ := t.Headers.JWK().Thumbprint(crypto.SHA256) + key, ok := t.Headers.JWK() + if !ok { + return false, errors.New("missing jwk header") + } + tp, _ := key.Thumbprint(crypto.SHA256) base64tp := base64.RawURLEncoding.EncodeToString(tp) if base64tp != jkt { diff --git a/crypto/dpop/dpop_test.go b/crypto/dpop/dpop_test.go index 908fb8c7de..151321e179 100644 --- a/crypto/dpop/dpop_test.go +++ b/crypto/dpop/dpop_test.go @@ -24,13 +24,13 @@ import ( "crypto/elliptic" "crypto/rand" "encoding/base64" - "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v3/jwk" "net/http" "testing" - "github.com/lestrrat-go/jwx/v2/jwa" - "github.com/lestrrat-go/jwx/v2/jws" - "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/lestrrat-go/jwx/v3/jwa" + "github.com/lestrrat-go/jwx/v3/jws" + "github.com/lestrrat-go/jwx/v3/jwt" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -42,12 +42,13 @@ func TestNewDPoP(t *testing.T) { token := New(*request) require.NotNil(t, token) - assert.Equal(t, DPopType, token.Headers.Type()) + headerType, _ := token.Headers.Type() + assert.Equal(t, DPopType, headerType) assert.Equal(t, "POST", token.HTM()) assert.Equal(t, "https://server.example.com/token", token.HTU()) // check if jti is set - jti, ok := token.Token.Get(jwt.JwtIDKey) - require.True(t, ok) + var jti string + require.NoError(t, token.Token.Get(jwt.JwtIDKey, &jti)) assert.NotEmpty(t, jti) }) } @@ -59,14 +60,14 @@ func TestDPoP_Proof(t *testing.T) { token := New(*request) token.GenerateProof("token") - ath, ok := token.Token.Get(ATHKey) - require.True(t, ok) + var ath string + require.NoError(t, token.Token.Get(ATHKey, &ath)) assert.Equal(t, "PEaenWxYddN6Q_NT1PiOYfz4EsZu7jRXRlpAsNpBU-A", ath) }) } func TestDPoP_Sign(t *testing.T) { - const alg = jwa.ES256 + var alg = jwa.ES256() keyPair, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) request, _ := http.NewRequest("POST", "https://server.example.com/token", nil) @@ -80,9 +81,10 @@ func TestDPoP_Sign(t *testing.T) { // check if jwk header is set and if the private part of the is omitted message, err := jws.ParseString(tokenString) require.NoError(t, err) - actualJWK, ok := message.Signatures()[0].ProtectedHeaders().Get(jws.JWKKey) - require.True(t, ok) - assert.Equal(t, alg, actualJWK.(jwk.Key).Algorithm()) + var actualJWK jwk.Key + require.NoError(t, message.Signatures()[0].ProtectedHeaders().Get(jws.JWKKey, &actualJWK)) + actualAlg, _ := actualJWK.Algorithm() + assert.Equal(t, alg, actualAlg) assert.Equal(t, "kid", token.Kid) }) t.Run("already signed", func(t *testing.T) { @@ -97,10 +99,10 @@ func TestDPoP_Sign(t *testing.T) { } func TestParseDPoP(t *testing.T) { - const alg = jwa.ES256 + var alg = jwa.ES256() keyPair, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - jwkKey, _ := jwk.FromRaw(keyPair) - _ = jwkKey.Set(jwk.AlgorithmKey, jwa.ES256) + jwkKey, _ := jwk.Import(keyPair) + _ = jwkKey.Set(jwk.AlgorithmKey, jwa.ES256()) pkey, _ := jwkKey.PublicKey() request, _ := http.NewRequest("GET", "https://server.example.com/token", nil) @@ -112,7 +114,8 @@ func TestParseDPoP(t *testing.T) { token, err := Parse(dpopString) require.NoError(t, err) - assert.Equal(t, pkey, token.Headers.JWK()) + headerJWK, _ := token.Headers.JWK() + assert.Equal(t, pkey, headerJWK) assert.Equal(t, "GET", token.HTM()) assert.Equal(t, "https://server.example.com/token", token.HTU()) }) @@ -120,7 +123,7 @@ func TestParseDPoP(t *testing.T) { _, err := Parse("invalid") require.Error(t, err) - assert.EqualError(t, err, "invalid DPoP token\ninvalid compact serialization format: invalid number of segments") + assert.EqualError(t, err, "invalid DPoP token\njws.ParseString: failed to parse string: jws.Parse: failed to parse compact format: jws.Parse: invalid compact serialization format: jwsbb: invalid number of segments") }) t.Run("unsupported algorithm", func(t *testing.T) { customJwt := jwt.New() @@ -147,7 +150,7 @@ func TestParseDPoP(t *testing.T) { altHeaders := jws.NewHeaders() altHeaders.Set(jws.TypeKey, DPopType) - tokenBytes, _ := jwt.Sign(altToken, jwt.WithKey(jwa.SignatureAlgorithm(jwkKey.Algorithm().String()), jwkKey, jws.WithProtectedHeaders(altHeaders))) + tokenBytes, _ := jwt.Sign(altToken, jwt.WithKey(alg, jwkKey, jws.WithProtectedHeaders(altHeaders))) _, err := Parse(string(tokenBytes)) @@ -160,7 +163,7 @@ func TestParseDPoP(t *testing.T) { altHeaders.Set(jws.TypeKey, DPopType) altHeaders.Set(jws.JWKKey, jwkKey) - tokenBytes, _ := jwt.Sign(altToken, jwt.WithKey(jwa.SignatureAlgorithm(jwkKey.Algorithm().String()), jwkKey, jws.WithProtectedHeaders(altHeaders))) + tokenBytes, _ := jwt.Sign(altToken, jwt.WithKey(alg, jwkKey, jws.WithProtectedHeaders(altHeaders))) _, err := Parse(string(tokenBytes)) @@ -174,7 +177,7 @@ func TestParseDPoP(t *testing.T) { _, err := Parse(dpopString + "0") require.Error(t, err) - assert.EqualError(t, err, "invalid DPoP token\ncould not verify message using any of the signatures or keys") + assert.EqualError(t, err, "invalid DPoP token\njwt.ParseString: failed to parse string: signature verification failed for ES256: dsig.VerifyECDSA: failed to unpack ECDSA signature: invalid signature length for curve \"P-256\"") }) t.Run("missing iat claim", func(t *testing.T) { dpopToken := New(*request) @@ -232,11 +235,11 @@ func TestParseDPoP(t *testing.T) { } func TestDPoP_Match(t *testing.T) { - const alg = jwa.ES256 + var alg = jwa.ES256() accessToken := "token" keyPair, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - jwkKey, _ := jwk.FromRaw(keyPair) - _ = jwkKey.Set(jwk.AlgorithmKey, jwa.ES256) + jwkKey, _ := jwk.Import(keyPair) + _ = jwkKey.Set(jwk.AlgorithmKey, jwa.ES256()) thumbprint, _ := jwkKey.Thumbprint(crypto.SHA256) thumbprintString := base64.RawURLEncoding.EncodeToString(thumbprint) request, _ := http.NewRequest("POST", "https://server.example.com/token", nil) @@ -317,10 +320,10 @@ func TestDPoP_Match(t *testing.T) { } func TestDPoP_marshalling(t *testing.T) { - const alg = jwa.ES256 + var alg = jwa.ES256() keyPair, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - jwkKey, _ := jwk.FromRaw(keyPair) - _ = jwkKey.Set(jwk.AlgorithmKey, jwa.ES256) + jwkKey, _ := jwk.Import(keyPair) + _ = jwkKey.Set(jwk.AlgorithmKey, jwa.ES256()) request, _ := http.NewRequest("POST", "https://server.example.com/token", nil) t.Run("marshal", func(t *testing.T) { diff --git a/crypto/dpop_test.go b/crypto/dpop_test.go index 69b2f74427..cc866f3c10 100644 --- a/crypto/dpop_test.go +++ b/crypto/dpop_test.go @@ -23,8 +23,8 @@ import ( "net/http" "testing" - "github.com/lestrrat-go/jwx/v2/jwa" - "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v3/jwa" + "github.com/lestrrat-go/jwx/v3/jwk" "github.com/nuts-foundation/nuts-node/audit" "github.com/nuts-foundation/nuts-node/crypto/dpop" "github.com/nuts-foundation/nuts-node/crypto/hash" @@ -36,8 +36,8 @@ func TestDPOP(t *testing.T) { client := createCrypto(t) kid := "kid" _, pubKey := newKeyReference(t, client, kid) - keyAsJWK, _ := jwk.FromRaw(pubKey) - _ = keyAsJWK.Set(jwk.AlgorithmKey, jwa.ES256) + keyAsJWK, _ := jwk.Import(pubKey) + _ = keyAsJWK.Set(jwk.AlgorithmKey, jwa.ES256()) request, _ := http.NewRequest("POST", "https://server.example.com/token", nil) t.Run("creates valid DPoP token", func(t *testing.T) { @@ -48,7 +48,8 @@ func TestDPOP(t *testing.T) { token, err = dpop.Parse(tokenString) require.NoError(t, err) - assert.Equal(t, keyAsJWK, token.Headers.JWK()) + headerJWK, _ := token.Headers.JWK() + assert.Equal(t, keyAsJWK, headerJWK) assert.Equal(t, "POST", token.HTM()) assert.Equal(t, "https://server.example.com/token", token.HTU()) }) @@ -65,8 +66,8 @@ func TestDPOP(t *testing.T) { proof, err := dpop.Parse(proofString) require.NoError(t, err) - ath, ok := proof.Token.Get(dpop.ATHKey) - require.True(t, ok) + var ath string + require.NoError(t, proof.Token.Get(dpop.ATHKey, &ath)) assert.Equal(t, hashString, ath) }) } diff --git a/crypto/jwx.go b/crypto/jwx.go index e9bd81fcd1..56d5e2d1a5 100644 --- a/crypto/jwx.go +++ b/crypto/jwx.go @@ -29,11 +29,11 @@ import ( "fmt" "maps" - "github.com/lestrrat-go/jwx/v2/jwa" - "github.com/lestrrat-go/jwx/v2/jwe" - "github.com/lestrrat-go/jwx/v2/jwk" - "github.com/lestrrat-go/jwx/v2/jws" - "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/lestrrat-go/jwx/v3/jwa" + "github.com/lestrrat-go/jwx/v3/jwe" + "github.com/lestrrat-go/jwx/v3/jwk" + "github.com/lestrrat-go/jwx/v3/jws" + "github.com/lestrrat-go/jwx/v3/jwt" "github.com/mr-tron/base58" "github.com/nuts-foundation/nuts-node/audit" "github.com/nuts-foundation/nuts-node/crypto/jwx" @@ -48,11 +48,11 @@ func GenerateJWK() (jwk.Key, error) { if err != nil { return nil, nil } - result, err := jwk.FromRaw(keyPair) + result, err := jwk.Import(keyPair) if err != nil { return nil, err } - return result, result.Set(jwk.AlgorithmKey, jwa.ES256) + return result, result.Set(jwk.AlgorithmKey, jwa.ES256()) } // SignJWT creates a JWT from the given claims and signs it with the given key. @@ -100,7 +100,7 @@ func (client *Crypto) DecryptJWE(ctx context.Context, message string) (body []by } protectedHeaders := msg.ProtectedHeaders() - kid := protectedHeaders.KeyID() + kid, _ := protectedHeaders.KeyID() if len(kid) == 0 { return nil, nil, errors.New("kid header not found") } @@ -111,18 +111,16 @@ func (client *Crypto) DecryptJWE(ctx context.Context, message string) (body []by audit.Log(ctx, log.Logger(), audit.CryptoDecryptJWEEvent).Infof("Decrypting a JWE with kid: %s", kid) - keyJWK, err := jwk.FromRaw(privateKey) + keyJWK, err := jwk.Import(privateKey) if err != nil { return nil, nil, fmt.Errorf("keys stored in '%s' do not support JWE decryption", client.backend.Name()) } - body, err = jwe.Decrypt([]byte(message), jwe.WithKey(protectedHeaders.Algorithm(), keyJWK)) - if err != nil { - return nil, nil, err - } - headers, err = msg.ProtectedHeaders().AsMap(ctx) + alg, _ := protectedHeaders.Algorithm() + body, err = jwe.Decrypt([]byte(message), jwe.WithKey(alg, keyJWK)) if err != nil { return nil, nil, err } + headers = headersAsMap(protectedHeaders) return body, headers, err } @@ -143,7 +141,7 @@ func SignJWT(ctx context.Context, key crypto.Signer, alg jwa.SignatureAlgorithm, return "", fmt.Errorf("invalid JWT headers: %w", err) } - sig, err = jwt.Sign(t, jwt.WithKey(jwa.SignatureAlgorithm(alg.String()), key, jws.WithProtectedHeaders(hdr))) + sig, err = jwt.Sign(t, jwt.WithKey(alg, key, jws.WithProtectedHeaders(hdr))) token = string(sig) return @@ -153,16 +151,18 @@ func SignJWT(ctx context.Context, key crypto.Signer, alg jwa.SignatureAlgorithm, func JWTKidAlg(tokenString string) (string, jwa.SignatureAlgorithm, error) { j, err := jws.ParseString(tokenString) if err != nil { - return "", "", err + return "", jwa.SignatureAlgorithm{}, err } if len(j.Signatures()) != 1 { - return "", "", errors.New("incorrect number of signatures in JWT") + return "", jwa.SignatureAlgorithm{}, errors.New("incorrect number of signatures in JWT") } sig := j.Signatures()[0] hdrs := sig.ProtectedHeaders() - return hdrs.KeyID(), hdrs.Algorithm(), nil + kid, _ := hdrs.KeyID() + alg, _ := hdrs.Algorithm() + return kid, alg, nil } // PublicKeyFunc defines a function that resolves a public key based on a kid @@ -206,17 +206,12 @@ func ParseJWS(token []byte, f PublicKeyFunc) (payload []byte, err error) { for i := range signatures { signature := signatures[i] // Get and check the algorithm - alg := signature.ProtectedHeaders().Algorithm() + alg, _ := signature.ProtectedHeaders().Algorithm() if !jwx.IsAlgorithmSupported(alg) { return nil, fmt.Errorf("token signing algorithm is not supported: %s", alg) } - // Get the verifier for the algorithm - verifier, err := jws.NewVerifier(alg) - if err != nil { - return nil, err - } // Get the key id, and get the associated key - kid := signature.ProtectedHeaders().KeyID() + kid, _ := signature.ProtectedHeaders().KeyID() key, err := f(kid) if err != nil { return nil, err @@ -227,10 +222,13 @@ func ParseJWS(token []byte, f PublicKeyFunc) (payload []byte, err error) { for _, part := range parts { payload = append(payload, part...) } - err = verifier.Verify(payload, signature.Signature(), key) + verifier, err := jws.VerifierFor(alg) if err != nil { return nil, err } + if err = verifier.Verify(key, payload, signature.Signature()); err != nil { + return nil, err + } } body = message.Payload() @@ -245,7 +243,7 @@ func SignJWS(ctx context.Context, payload []byte, protectedHeaders map[string]in return "", fmt.Errorf("unable to set header %s: %w", key, err) } } - if headers.JWK() != nil { + if headerJWK, ok := headers.JWK(); ok { // 'kid' has been logged, use 'jwk' to sign _ = headers.Remove(jwk.KeyIDKey) @@ -253,7 +251,7 @@ func SignJWS(ctx context.Context, payload []byte, protectedHeaders map[string]in // we want to make sure the `jwk` header (if present) does not (accidentally) contain a private key. // That would lead to the node leaking its private key material in the resulting JWS which would be very, very bad. var jwkAsPrivateKey crypto.Signer - if err := headers.JWK().Raw(&jwkAsPrivateKey); err == nil { + if err := jwk.Export(headerJWK, &jwkAsPrivateKey); err == nil { // `err != nil` is good in this case, because that means the key is not assignable to crypto.Signer, // which is the interface implemented by all private key types. return "", errors.New("refusing to sign JWS with private key in JWK header") @@ -286,19 +284,19 @@ func EncryptJWE(payload []byte, protectedHeaders map[string]interface{}, publicK if publicKey == nil { return "", errors.New("no publicKey provided") } - json, err := json.Marshal(protectedHeaders) + headersJSON, err := json.Marshal(protectedHeaders) if err != nil { return "", err } headers := jwe.NewHeaders() - err = headers.UnmarshalJSON(json) + err = json.Unmarshal(headersJSON, headers) if err != nil { return "", err } // Figure out the KeyEncryptionAlgorithm, give prevalence to the headers var alg jwa.KeyEncryptionAlgorithm - if len(headers.Algorithm().String()) > 0 { - alg = headers.Algorithm() + if hdrAlg, ok := headers.Algorithm(); ok && len(hdrAlg.String()) > 0 { + alg = hdrAlg } else { alg, err = encryptionAlgorithm(publicKey) if err != nil { @@ -308,14 +306,15 @@ func EncryptJWE(payload []byte, protectedHeaders map[string]interface{}, publicK // Figure out the KeyEncryptionAlgorithm, give prevalence to the headers enc := jwx.DefaultContentEncryptionAlgorithm - if len(headers.ContentEncryption().String()) > 0 { - enc = headers.ContentEncryption() + if hdrEnc, ok := headers.ContentEncryption(); ok && len(hdrEnc.String()) > 0 { + enc = hdrEnc } + compression, _ := headers.Compression() options := []jwe.EncryptOption{ jwe.WithProtectedHeaders(headers), jwe.WithContentEncryption(enc), jwe.WithKey(alg, publicKey), - jwe.WithCompress(headers.Compression()), // "" means no compression + jwe.WithCompress(compression), // "" means no compression } encoded, err := jwe.Encrypt(payload, options...) @@ -334,16 +333,39 @@ func ExtractProtectedHeaders(jwt string) (map[string]interface{}, error) { if len(message.Signatures()) != 1 { return nil, ErrorInvalidNumberOfSignatures } - var err error - headers, err = message.Signatures()[0].ProtectedHeaders().AsMap(context.Background()) - if err != nil { - return nil, err - } + headers = headersAsMap(message.Signatures()[0].ProtectedHeaders()) } } return headers, nil } +// headersAsMap converts jws or jwe protected headers to a map of their members, keyed by JSON +// member name. +// +// Unlike jwx.ClaimsAsMap it iterates the members with Get, preserving the concrete Go types of +// rich members such as the x5c certificate chain (a JSON round-trip would flatten those). It +// replaces jwx v2's Headers.AsMap: a null-valued member is stored as nil rather than causing an +// error, because v3's per-field Get fails on a null value and would otherwise reject an +// otherwise-valid header set. +func headersAsMap(headers interface { + Keys() []string + Get(string, any) error +}) map[string]interface{} { + result := make(map[string]interface{}) + for _, k := range headers.Keys() { + var v interface{} + if err := headers.Get(k, &v); err != nil { + // k comes from Keys() so the member exists, and the destination is interface{}, + // which accepts any non-nil value; the only failure mode is a null-valued member. + // Represent it as nil, matching v2's Headers.AsMap. + result[k] = nil + continue + } + result[k] = v + } + return result +} + func (client *Crypto) getPrivateKey(ctx context.Context, kid string) (crypto.Signer, string, error) { keyRef, err := client.findKeyReferenceByKid(ctx, kid) if err != nil { @@ -373,24 +395,24 @@ func convertHeaders(headers map[string]interface{}) (jws.Headers, error) { func signingAlg(key crypto.PublicKey) (jwa.SignatureAlgorithm, error) { switch k := key.(type) { case *rsa.PublicKey: - return jwa.PS256, nil + return jwa.PS256(), nil case *ecdsa.PublicKey: return ecAlgUsingPublicKey(*k) case ed25519.PublicKey: - return jwa.EdDSA, nil + return jwa.EdDSA(), nil default: - return "", fmt.Errorf(`could not determine signature algorithm for key type '%T'`, key) + return jwa.SignatureAlgorithm{}, fmt.Errorf(`could not determine signature algorithm for key type '%T'`, key) } } func ecAlgUsingPublicKey(key ecdsa.PublicKey) (alg jwa.SignatureAlgorithm, err error) { switch key.Params().BitSize { case 256: - alg = jwa.ES256 + alg = jwa.ES256() case 384: - alg = jwa.ES384 + alg = jwa.ES384() case 521: - alg = jwa.ES512 + alg = jwa.ES512() default: err = jwx.ErrUnsupportedSigningKey } @@ -400,7 +422,7 @@ func ecAlgUsingPublicKey(key ecdsa.PublicKey) (alg jwa.SignatureAlgorithm, err e // SignatureAlgorithm determines the jwa.SigningAlgorithm for ec/rsa/ed25519 keys. func SignatureAlgorithm(key crypto.PublicKey) (jwa.SignatureAlgorithm, error) { if key == nil { - return "", errors.New("no key provided") + return jwa.SignatureAlgorithm{}, errors.New("no key provided") } var ptr interface{} @@ -419,19 +441,19 @@ func SignatureAlgorithm(key crypto.PublicKey) (jwa.SignatureAlgorithm, error) { switch k := ptr.(type) { case *rsa.PrivateKey: - return jwa.PS256, nil + return jwa.PS256(), nil case *rsa.PublicKey: - return jwa.PS256, nil + return jwa.PS256(), nil case *ecdsa.PrivateKey: return ecAlgUsingPublicKey(k.PublicKey) case *ecdsa.PublicKey: return ecAlgUsingPublicKey(*k) case ed25519.PrivateKey: - return jwa.EdDSA, nil + return jwa.EdDSA(), nil case ed25519.PublicKey: - return jwa.EdDSA, nil + return jwa.EdDSA(), nil default: - return "", fmt.Errorf(`could not determine signature algorithm for key type '%T'`, key) + return jwa.SignatureAlgorithm{}, fmt.Errorf(`could not determine signature algorithm for key type '%T'`, key) } } @@ -443,7 +465,7 @@ func encryptionAlgorithm(key crypto.PublicKey) (jwa.KeyEncryptionAlgorithm, erro case *ecdsa.PublicKey: return jwx.DefaultEcEncryptionAlgorithm, nil default: - return "", fmt.Errorf("could not determine encryption algorithm for key type '%T'", key) + return jwa.KeyEncryptionAlgorithm{}, fmt.Errorf("could not determine encryption algorithm for key type '%T'", key) } } diff --git a/crypto/jwx/algorithm.go b/crypto/jwx/algorithm.go index 4f89d7684e..6b41f9b75d 100644 --- a/crypto/jwx/algorithm.go +++ b/crypto/jwx/algorithm.go @@ -21,17 +21,17 @@ package jwx import ( "errors" - "github.com/lestrrat-go/jwx/v2/jwa" + "github.com/lestrrat-go/jwx/v3/jwa" ) // ErrUnsupportedSigningKey is returned when an unsupported private key is used to sign. Currently only ecdsa and rsa keys are supported var ErrUnsupportedSigningKey = errors.New("signing key algorithm not supported") -var SupportedAlgorithms = []jwa.SignatureAlgorithm{jwa.ES256, jwa.EdDSA, jwa.ES384, jwa.ES512, jwa.PS256, jwa.PS384, jwa.PS512} +var SupportedAlgorithms = []jwa.SignatureAlgorithm{jwa.ES256(), jwa.EdDSA(), jwa.ES384(), jwa.ES512(), jwa.PS256(), jwa.PS384(), jwa.PS512()} -const DefaultRsaEncryptionAlgorithm = jwa.RSA_OAEP_256 -const DefaultEcEncryptionAlgorithm = jwa.ECDH_ES_A256KW -const DefaultContentEncryptionAlgorithm = jwa.A256GCM +var DefaultRsaEncryptionAlgorithm = jwa.RSA_OAEP_256() +var DefaultEcEncryptionAlgorithm = jwa.ECDH_ES_A256KW() +var DefaultContentEncryptionAlgorithm = jwa.A256GCM() func IsAlgorithmSupported(alg jwa.SignatureAlgorithm) bool { for _, curr := range SupportedAlgorithms { @@ -51,7 +51,7 @@ func AddSupportedAlgorithm(alg jwa.SignatureAlgorithm) bool { func SupportedAlgorithmsAsStrings() []string { var result []string for _, alg := range SupportedAlgorithms { - result = append(result, string(alg)) + result = append(result, alg.String()) } return result } diff --git a/crypto/jwx/claims.go b/crypto/jwx/claims.go new file mode 100644 index 0000000000..3399d1ddf2 --- /dev/null +++ b/crypto/jwx/claims.go @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2024 Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package jwx + +import ( + "encoding/json" + + "github.com/lestrrat-go/jwx/v3/jwt" +) + +// ClaimsAsMap returns a JWT token's claims as a map, keyed by their JSON member names. +// +// It replaces jwx v2's token.AsMap, which was removed in v3. Marshaling via JSON (rather than +// iterating keys and calling Get per claim) is deliberate: v3's per-field Get returns an error +// for a null-valued claim, whereas the JSON round-trip preserves null claims as nil - matching +// v2's AsMap behaviour and avoiding rejection of otherwise-valid tokens. +func ClaimsAsMap(token jwt.Token) (map[string]interface{}, error) { + data, err := json.Marshal(token) + if err != nil { + return nil, err + } + result := make(map[string]interface{}) + if err := json.Unmarshal(data, &result); err != nil { + return nil, err + } + return result, nil +} diff --git a/crypto/jwx/jwx_es256k.go b/crypto/jwx/jwx_es256k.go index d8a495a790..6ba2ba0cc3 100644 --- a/crypto/jwx/jwx_es256k.go +++ b/crypto/jwx/jwx_es256k.go @@ -20,8 +20,8 @@ package jwx -import "github.com/lestrrat-go/jwx/v2/jwa" +import "github.com/lestrrat-go/jwx/v3/jwa" func init() { - AddSupportedAlgorithm(jwa.ES256K) + AddSupportedAlgorithm(jwa.ES256K()) } diff --git a/crypto/jwx/jwx_es256k_test.go b/crypto/jwx_es256k_test.go similarity index 52% rename from crypto/jwx/jwx_es256k_test.go rename to crypto/jwx_es256k_test.go index b2bb75b6eb..e487ea1bbe 100644 --- a/crypto/jwx/jwx_es256k_test.go +++ b/crypto/jwx_es256k_test.go @@ -18,28 +18,35 @@ * */ -package jwx +package crypto import ( "crypto" - "github.com/lestrrat-go/jwx/v2/jwa" - "github.com/lestrrat-go/jwx/v2/jwt" - "github.com/nuts-foundation/nuts-node/crypto/test" + "crypto/ecdsa" + "crypto/rand" + "testing" + + "github.com/decred/dcrd/dcrec/secp256k1/v4" + "github.com/lestrrat-go/jwx/v3/jwa" + "github.com/lestrrat-go/jwx/v3/jwt" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "testing" ) +// TestES256k verifies that, when built with the jwx_es256k tag, an ES256K-signed JWT +// round-trips through ParseJWT (i.e. ES256K is registered as a supported algorithm). func TestES256k(t *testing.T) { - t.Run("test ES256K", func(t *testing.T) { - ecKey := test.GenerateECKey() - token := jwt.New() - signature, _ := jwt.Sign(token, jwt.WithKey(jwa.ES256K, ecKey)) - parsedToken, err := ParseJWT(string(signature), func(_ string) (crypto.PublicKey, error) { - return ecKey.Public(), nil - }) - require.NoError(t, err) + // ES256K signs over the secp256k1 curve, so a P-256 key won't do. + ecKey, err := ecdsa.GenerateKey(secp256k1.S256(), rand.Reader) + require.NoError(t, err) + + token := jwt.New() + signature, err := jwt.Sign(token, jwt.WithKey(jwa.ES256K(), ecKey)) + require.NoError(t, err) - assert.NotNil(t, parsedToken) + parsedToken, err := ParseJWT(string(signature), func(_ string) (crypto.PublicKey, error) { + return ecKey.Public(), nil }) + require.NoError(t, err) + assert.NotNil(t, parsedToken) } diff --git a/crypto/jwx_test.go b/crypto/jwx_test.go index b42e45a5dc..13c48dc361 100644 --- a/crypto/jwx_test.go +++ b/crypto/jwx_test.go @@ -37,11 +37,11 @@ import ( "testing" "time" - "github.com/lestrrat-go/jwx/v2/jwa" - "github.com/lestrrat-go/jwx/v2/jwe" - "github.com/lestrrat-go/jwx/v2/jwk" - "github.com/lestrrat-go/jwx/v2/jws" - "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/lestrrat-go/jwx/v3/jwa" + "github.com/lestrrat-go/jwx/v3/jwe" + "github.com/lestrrat-go/jwx/v3/jwk" + "github.com/lestrrat-go/jwx/v3/jws" + "github.com/lestrrat-go/jwx/v3/jwt" "github.com/mr-tron/base58" "github.com/nuts-foundation/nuts-node/audit" "github.com/nuts-foundation/nuts-node/crypto/test" @@ -60,7 +60,7 @@ func TestSignJWT(t *testing.T) { claims := map[string]interface{}{"iss": "nuts"} t.Run("creates valid JWT using rsa keys", func(t *testing.T) { rsaKey := test.GenerateRSAKey() - tokenString, err := SignJWT(audit.TestContext(), rsaKey, jwa.PS256, claims, nil) + tokenString, err := SignJWT(audit.TestContext(), rsaKey, jwa.PS256(), claims, nil) assert.Nil(t, err) @@ -70,7 +70,8 @@ func TestSignJWT(t *testing.T) { require.NoError(t, err) - assert.Equal(t, "nuts", token.Issuer()) + issuer, _ := token.Issuer() + assert.Equal(t, "nuts", issuer) }) t.Run("creates valid JWT using ec keys", func(t *testing.T) { @@ -79,7 +80,7 @@ func TestSignJWT(t *testing.T) { p521, _ := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) keys := []*ecdsa.PrivateKey{p256, p384, p521} - algs := []jwa.SignatureAlgorithm{jwa.ES256, jwa.ES384, jwa.ES512} + algs := []jwa.SignatureAlgorithm{jwa.ES256(), jwa.ES384(), jwa.ES512()} for i, ecKey := range keys { name := fmt.Sprintf("using %s", ecKey.Params().Name) @@ -93,26 +94,28 @@ func TestSignJWT(t *testing.T) { }) require.NoError(t, err) - assert.Equal(t, "nuts", token.Issuer()) + issuer, _ := token.Issuer() + assert.Equal(t, "nuts", issuer) }) } }) t.Run("sets correct headers", func(t *testing.T) { ecKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - tokenString, err := SignJWT(audit.TestContext(), ecKey, jwa.ES256, claims, nil) + tokenString, err := SignJWT(audit.TestContext(), ecKey, jwa.ES256(), claims, nil) require.NoError(t, err) msg, err := jws.ParseString(tokenString) require.NoError(t, err) hdrs := msg.Signatures()[0].ProtectedHeaders() - alg, _ := hdrs.Get(jwk.AlgorithmKey) - assert.Equal(t, jwa.ES256, alg) + var alg jwa.SignatureAlgorithm + require.NoError(t, hdrs.Get(jwk.AlgorithmKey, &alg)) + assert.Equal(t, jwa.ES256(), alg) }) t.Run("invalid claim", func(t *testing.T) { - tokenString, err := SignJWT(audit.TestContext(), nil, jwa.ES256, map[string]interface{}{jwt.IssuedAtKey: "foobar"}, nil) + tokenString, err := SignJWT(audit.TestContext(), nil, jwa.ES256(), map[string]interface{}{jwt.IssuedAtKey: "foobar"}, nil) assert.Empty(t, tokenString) assert.EqualError(t, err, "invalid value for iat key: failed to accept string \"foobar\": value is not number of seconds since the epoch, and attempt to parse it as RFC3339 timestamp failed: parsing time \"foobar\" as \"2006-01-02T15:04:05Z07:00\": cannot parse \"foobar\" as \"2006\"") }) @@ -122,7 +125,7 @@ func TestParseJWT(t *testing.T) { t.Run("unsupported algorithm", func(t *testing.T) { rsaKey := test.GenerateRSAKey() token := jwt.New() - signature, _ := jwt.Sign(token, jwt.WithKey(jwa.RS256, rsaKey)) + signature, _ := jwt.Sign(token, jwt.WithKey(jwa.RS256(), rsaKey)) parsedToken, err := ParseJWT(string(signature), func(_ string) (crypto.PublicKey, error) { return rsaKey.Public(), nil }) @@ -135,7 +138,7 @@ func TestParseJWT(t *testing.T) { token := jwt.New() err := token.Set(jwt.IssuedAtKey, time.Now().Add(4*time.Second).Unix()) assert.NoError(t, err) - signature, _ := jwt.Sign(token, jwt.WithKey(jwa.ES256, ecKey)) + signature, _ := jwt.Sign(token, jwt.WithKey(jwa.ES256(), ecKey)) parsedToken, err := ParseJWT(string(signature), func(_ string) (crypto.PublicKey, error) { return ecKey.Public(), nil }, jwt.WithAcceptableSkew(5000*time.Millisecond)) @@ -148,14 +151,14 @@ func TestParseJWT(t *testing.T) { authenticKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) attackerKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) token := jwt.New() - validToken, _ := jwt.Sign(token, jwt.WithKey(jwa.ES256, authenticKey)) + validToken, _ := jwt.Sign(token, jwt.WithKey(jwa.ES256(), authenticKey)) parsedToken, err := ParseJWT(string(validToken), func(_ string) (crypto.PublicKey, error) { return attackerKey.Public(), nil }) assert.Nil(t, parsedToken) - assert.EqualError(t, err, "could not verify message using any of the signatures or keys") + assert.EqualError(t, err, "jwt.ParseString: failed to parse string: signature verification failed for ES256: invalid ECDSA signature") }) } @@ -178,7 +181,8 @@ func TestCrypto_SignJWT(t *testing.T) { require.NoError(t, err) - assert.Equal(t, "nuts", token.Issuer()) + issuer, _ := token.Issuer() + assert.Equal(t, "nuts", issuer) assert.Equal(t, kid, actualKID) }) t.Run("creates valid JWT using external key", func(t *testing.T) { @@ -203,7 +207,8 @@ func TestCrypto_SignJWT(t *testing.T) { require.NoError(t, err) - assert.Equal(t, "nuts", token.Issuer()) + issuer, _ := token.Issuer() + assert.Equal(t, "nuts", issuer) assert.Equal(t, kid, actualKID) }) t.Run("writes audit logs", func(t *testing.T) { @@ -285,7 +290,7 @@ func TestCrypto_SignJWS(t *testing.T) { auditLogs := audit.CaptureAuditLogs(t) key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(t, err) - publicKeyAsJWK, _ := jwk.FromRaw(key.Public()) + publicKeyAsJWK, _ := jwk.Import(key.Public()) hdrs := map[string]interface{}{ "kid": kid, "jwk": publicKeyAsJWK, @@ -297,7 +302,8 @@ func TestCrypto_SignJWS(t *testing.T) { auditLogs.AssertContains(t, ModuleName, "SignJWS", audit.TestActor, "Signing a JWS with key: kid") // kid is not in headers msg, err := jws.Parse([]byte(signature)) - assert.Empty(t, msg.Signatures()[0].ProtectedHeaders().KeyID()) + kid, _ := msg.Signatures()[0].ProtectedHeaders().KeyID() + assert.Empty(t, kid) }) t.Run("returns error for not found", func(t *testing.T) { @@ -361,7 +367,7 @@ func TestCrypto_EncryptJWE(t *testing.T) { privateKey, _, err := client.getPrivateKey(context.Background(), kid) require.NoError(t, err) - token, err := jwe.Decrypt([]byte(tokenString), jwe.WithKey(jwa.ECDH_ES, privateKey)) + token, err := jwe.Decrypt([]byte(tokenString), jwe.WithKey(jwa.ECDH_ES(), privateKey)) require.NoError(t, err) var body = make(map[string]interface{}) @@ -493,19 +499,21 @@ func TestSignJWS(t *testing.T) { sig := message.Signatures() assert.Len(t, sig, 1, "there must be one signature in the parsed message") - fooValue, _ := sig[0].ProtectedHeaders().Get("foo") - assert.Equal(t, "bar", fooValue.(string), + var fooValue string + _ = sig[0].ProtectedHeaders().Get("foo", &fooValue) + assert.Equal(t, "bar", fooValue, "the protected headers must contain the 'foo' key with 'bar' value") // Sanity check: verify signature - actualPayload, err := jws.Verify([]byte(signature), jws.WithKey(sig[0].ProtectedHeaders().Algorithm(), key.Public())) + sigAlg, _ := sig[0].ProtectedHeaders().Algorithm() + actualPayload, err := jws.Verify([]byte(signature), jws.WithKey(sigAlg, key.Public())) require.NoError(t, err, "the signature could not be validated") assert.Equal(t, payload, actualPayload) }) t.Run("public key in JWK header is allowed", func(t *testing.T) { payload := []byte{1, 2, 3} - publicKeyAsJWK, _ := jwk.FromRaw(key.Public()) + publicKeyAsJWK, _ := jwk.Import(key.Public()) hdrs := map[string]interface{}{"jwk": publicKeyAsJWK} signature, err := SignJWS(audit.TestContext(), payload, hdrs, key, false) assert.NoError(t, err) @@ -518,13 +526,13 @@ func TestSignJWS(t *testing.T) { payload := []byte{'.'} signature, err := SignJWS(audit.TestContext(), payload, hdrs, key, false) - assert.EqualError(t, err, "unable to sign JWS failed to generate signature for signer #0 (alg=ES256): payload must not contain a \".\"") + assert.EqualError(t, err, "unable to sign JWS jws.Sign: failed to populate message: failed to build signature 0: compact serialization with b64=false requires payload to contain no \".\" characters per RFC 7797 §5.2; use jws.WithDetachedPayload to keep the payload out of the wire format") assert.Empty(t, signature) }) t.Run("private key in JWK header is not allowed", func(t *testing.T) { payload := []byte{1, 2, 3} - privateKeyAsJWK, _ := jwk.FromRaw(key) + privateKeyAsJWK, _ := jwk.Import(key) hdrs := map[string]interface{}{"jwk": privateKeyAsJWK} signature, err := SignJWS(audit.TestContext(), payload, hdrs, key, false) assert.EqualError(t, err, "refusing to sign JWS with private key in JWK header") @@ -554,7 +562,7 @@ func TestCrypto_convertHeaders(t *testing.T) { t.Run("nil headers", func(t *testing.T) { jwtHeader, err := convertHeaders(nil) require.NoError(t, err) - assert.Len(t, jwtHeader.PrivateParams(), 0) + assert.Len(t, jwtHeader.Keys(), 0) }) t.Run("ok", func(t *testing.T) { @@ -563,7 +571,8 @@ func TestCrypto_convertHeaders(t *testing.T) { } jwtHeader, err := convertHeaders(rawHeaders) - v, _ := jwtHeader.Get("key") + var v string + require.NoError(t, jwtHeader.Get("key", &v)) require.NoError(t, err) assert.Equal(t, "value", v) }) @@ -580,9 +589,9 @@ func TestCrypto_convertHeaders(t *testing.T) { } func Test_isAlgorithmSupported(t *testing.T) { - assert.True(t, jwx.IsAlgorithmSupported(jwa.PS256)) - assert.False(t, jwx.IsAlgorithmSupported(jwa.RS256)) - assert.False(t, jwx.IsAlgorithmSupported("")) + assert.True(t, jwx.IsAlgorithmSupported(jwa.PS256())) + assert.False(t, jwx.IsAlgorithmSupported(jwa.RS256())) + assert.False(t, jwx.IsAlgorithmSupported(jwa.SignatureAlgorithm{})) } func TestSignatureAlgorithm(t *testing.T) { @@ -610,16 +619,16 @@ func TestSignatureAlgorithm(t *testing.T) { key interface{} alg jwa.SignatureAlgorithm }{ - {"EC private key as pointer", ecKey256, jwa.ES256}, - {"EC private key", *ecKey256, jwa.ES256}, - {"EC public key as pointer", &ecKey384.PublicKey, jwa.ES384}, - {"EC public key", ecKey521.PublicKey, jwa.ES512}, - {"RSA private key as pointer", rsaKey, jwa.PS256}, - {"RSA private key", *rsaKey, jwa.PS256}, - {"RSA public key as pointer", &rsaKey.PublicKey, jwa.PS256}, - {"RSA public key", rsaKey.PublicKey, jwa.PS256}, - {"ED25519 private key", pEDKey, jwa.EdDSA}, - {"ED25519 public key", sEDKey, jwa.EdDSA}, + {"EC private key as pointer", ecKey256, jwa.ES256()}, + {"EC private key", *ecKey256, jwa.ES256()}, + {"EC public key as pointer", &ecKey384.PublicKey, jwa.ES384()}, + {"EC public key", ecKey521.PublicKey, jwa.ES512()}, + {"RSA private key as pointer", rsaKey, jwa.PS256()}, + {"RSA private key", *rsaKey, jwa.PS256()}, + {"RSA public key as pointer", &rsaKey.PublicKey, jwa.PS256()}, + {"RSA public key", rsaKey.PublicKey, jwa.PS256()}, + {"ED25519 private key", pEDKey, jwa.EdDSA()}, + {"ED25519 public key", sEDKey, jwa.EdDSA()}, } for _, test := range tests { @@ -681,33 +690,33 @@ func Test_signingAlg(t *testing.T) { key, _ := rsa.GenerateKey(rand.Reader, 1024) alg, err := signingAlg(key.Public()) require.NoError(t, err) - assert.Equal(t, jwa.PS256, alg) + assert.Equal(t, jwa.PS256(), alg) }) t.Run("EC", func(t *testing.T) { t.Run("P256", func(t *testing.T) { key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) alg, err := signingAlg(key.Public()) require.NoError(t, err) - assert.Equal(t, jwa.ES256, alg) + assert.Equal(t, jwa.ES256(), alg) }) t.Run("P384", func(t *testing.T) { key, _ := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) alg, err := signingAlg(key.Public()) require.NoError(t, err) - assert.Equal(t, jwa.ES384, alg) + assert.Equal(t, jwa.ES384(), alg) }) t.Run("P521", func(t *testing.T) { key, _ := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) alg, err := signingAlg(key.Public()) require.NoError(t, err) - assert.Equal(t, jwa.ES512, alg) + assert.Equal(t, jwa.ES512(), alg) }) }) t.Run("ED25519", func(t *testing.T) { key, _, _ := ed25519.GenerateKey(rand.Reader) alg, err := signingAlg(key) require.NoError(t, err) - assert.Equal(t, jwa.EdDSA, alg) + assert.Equal(t, jwa.EdDSA(), alg) }) t.Run("unsupported key", func(t *testing.T) { _, err := signingAlg(nil) @@ -726,7 +735,7 @@ func TestExtractProtectedHeaders(t *testing.T) { if err != nil { return "", err } - sign, err := jws.Sign(marshal, jws.WithKey(jwa.ES256, jwk)) + sign, err := jws.Sign(marshal, jws.WithKey(jwa.ES256(), jwk)) if err != nil { return "", err } @@ -741,7 +750,7 @@ func TestExtractProtectedHeaders(t *testing.T) { if err != nil { return "", err } - sign, err := jws.Sign(marshal, jws.WithKey(jwa.ES256, jwk), jws.WithKey(jwa.ES256, jwk), jws.WithJSON()) + sign, err := jws.Sign(marshal, jws.WithKey(jwa.ES256(), jwk), jws.WithKey(jwa.ES256(), jwk), jws.WithJSON()) if err != nil { return "", err } diff --git a/crypto/memory.go b/crypto/memory.go index 56d93ab951..c3d5bd8086 100644 --- a/crypto/memory.go +++ b/crypto/memory.go @@ -24,7 +24,7 @@ import ( "errors" "maps" - "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v3/jwk" "github.com/nuts-foundation/nuts-node/crypto/dpop" ) @@ -42,11 +42,11 @@ func (m MemoryJWTSigner) SignJWT(ctx context.Context, claims map[string]interfac headersLocal := make(map[string]interface{}) maps.Copy(headersLocal, headers) - if kid != m.Key.KeyID() { + if keyID, _ := m.Key.KeyID(); kid != keyID { return "", ErrPrivateKeyNotFound } var signer crypto.Signer - if err := m.Key.Raw(&signer); err != nil { + if err := jwk.Export(m.Key, &signer); err != nil { return "", err } alg, err := signingAlg(signer.Public()) @@ -60,10 +60,10 @@ func (m MemoryJWTSigner) SignJWT(ctx context.Context, claims map[string]interfac func (m MemoryJWTSigner) SignJWS(ctx context.Context, payload []byte, headers map[string]interface{}, kid string, detached bool) (string, error) { var signer crypto.Signer - if err := m.Key.Raw(&signer); err != nil { + if err := jwk.Export(m.Key, &signer); err != nil { return "", err } - if kid != m.Key.KeyID() { + if keyID, _ := m.Key.KeyID(); kid != keyID { return "", ErrPrivateKeyNotFound } headers["kid"] = kid diff --git a/crypto/memory_test.go b/crypto/memory_test.go index ea2ea172a4..5171d06574 100644 --- a/crypto/memory_test.go +++ b/crypto/memory_test.go @@ -25,13 +25,13 @@ import ( "github.com/nuts-foundation/nuts-node/audit" "testing" - "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v3/jwk" "github.com/stretchr/testify/assert" ) func TestMemoryKeyStore_SignJWT(t *testing.T) { pk, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - privateKeyJWK, _ := jwk.FromRaw(pk) + privateKeyJWK, _ := jwk.Import(pk) privateKeyJWK.Set(jwk.KeyIDKey, "123") alg, _ := ecAlgUsingPublicKey(pk.PublicKey) privateKeyJWK.Set(jwk.AlgorithmKey, alg) @@ -53,7 +53,7 @@ func TestMemoryKeyStore_SignJWT(t *testing.T) { func TestMemoryJWTSigner_SignJWS(t *testing.T) { pk, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - privateKeyJWK, _ := jwk.FromRaw(pk) + privateKeyJWK, _ := jwk.Import(pk) privateKeyJWK.Set(jwk.KeyIDKey, "123") alg, _ := ecAlgUsingPublicKey(pk.PublicKey) privateKeyJWK.Set(jwk.AlgorithmKey, alg) diff --git a/crypto/storage/azure/keyvault.go b/crypto/storage/azure/keyvault.go index 7b0e233c49..1a49517e1d 100644 --- a/crypto/storage/azure/keyvault.go +++ b/crypto/storage/azure/keyvault.go @@ -33,7 +33,7 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/Azure/azure-sdk-for-go/sdk/azidentity" "github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys" - "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v3/jwk" "github.com/nuts-foundation/nuts-node/core" "github.com/nuts-foundation/nuts-node/crypto/log" "github.com/nuts-foundation/nuts-node/crypto/storage/spi" @@ -209,14 +209,14 @@ func parseKey(key *azkeys.JSONWebKey) (publicKey crypto.PublicKey, keyType azkey err = fmt.Errorf("unable to parse key from Azure Key Vault as JWK: %w", err) return } - if err = keyAsJWK.Raw(&publicKey); err != nil { - err = fmt.Errorf("unable to convert key from Azure Key Vault Key to crypto.PublicKey: %w", err) - return - } if !(*key.Kty == azkeys.KeyTypeEC || *key.Kty == azkeys.KeyTypeECHSM) || *key.Crv != azkeys.CurveNameP256 { err = errors.New("only ES256 keys are supported") return } + if err = jwk.Export(keyAsJWK, &publicKey); err != nil { + err = fmt.Errorf("unable to convert key from Azure Key Vault Key to crypto.PublicKey: %w", err) + return + } keyType = azkeys.SignatureAlgorithmES256 if key.KID == nil { err = errors.New("missing KID in key") diff --git a/crypto/storage/azure/keyvault_test.go b/crypto/storage/azure/keyvault_test.go index 37faf224b7..e2b7d5b73a 100644 --- a/crypto/storage/azure/keyvault_test.go +++ b/crypto/storage/azure/keyvault_test.go @@ -21,6 +21,7 @@ package azure import ( "context" "crypto/ecdsa" + "crypto/elliptic" "crypto/rand" "crypto/rsa" "crypto/sha256" @@ -37,7 +38,7 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys" "github.com/google/uuid" - "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v3/jwk" "github.com/nuts-foundation/nuts-node/crypto/storage/spi" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -112,8 +113,8 @@ func Test_Keyvault_GetPrivateKey(t *testing.T) { }) t.Run("unsupported key type", func(t *testing.T) { // Generate an RSA key to return - privateKey, _ := rsa.GenerateKey(rand.Reader, 1024) - privateKeyAsJWK, err := jwk.FromRaw(privateKey.PublicKey) + privateKey, _ := rsa.GenerateKey(rand.Reader, 2048) + privateKeyAsJWK, err := jwk.Import(privateKey.PublicKey) require.NoError(t, err) privateKeyJWKAsBytes_, _ := json.Marshal(privateKeyAsJWK) var jsonWebKey azkeys.JSONWebKey @@ -241,12 +242,20 @@ func Test_azureSigningKey_Sign(t *testing.T) { func keyBundle() azkeys.KeyBundle { id := azkeys.ID("https://myvaultname.vault.azure.net/keys/did-web-example-com-0/b86c2e6ad9054f4abf69cc185b99aa60") + // jwx v3 validates that the X/Y coordinates have the correct length for the curve, + // so use a real P-256 key's coordinates instead of arbitrary bytes. + key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + ecdhPub, _ := key.PublicKey.ECDH() + // Uncompressed point encoding: 0x04 || X (32 bytes) || Y (32 bytes). + point := ecdhPub.Bytes() + x := point[1:33] + y := point[33:65] return azkeys.KeyBundle{ Key: &azkeys.JSONWebKey{ Kty: to.Ptr(azkeys.KeyTypeEC), Crv: to.Ptr(azkeys.CurveNameP256), - X: []byte{1, 2, 3}, - Y: []byte{4, 5, 6}, + X: x, + Y: y, KID: &id, }, } diff --git a/crypto/storage/spi/interface.go b/crypto/storage/spi/interface.go index 0652c96eee..de532e7c93 100644 --- a/crypto/storage/spi/interface.go +++ b/crypto/storage/spi/interface.go @@ -29,7 +29,7 @@ import ( "fmt" "regexp" - "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v3/jwk" "github.com/nuts-foundation/nuts-node/core" ) @@ -77,10 +77,14 @@ type PublicKeyEntry struct { // FromJWK fills the publicKeyEntry with key material from the given key func (pke *PublicKeyEntry) FromJWK(key jwk.Key) error { - asMap, err := key.AsMap(context.Background()) + data, err := json.Marshal(key) if err != nil { return err } + asMap := make(map[string]interface{}) + if err := json.Unmarshal(data, &asMap); err != nil { + return err + } pke.Key = asMap pke.parsedJWK = key return nil diff --git a/crypto/storage/spi/interface_test.go b/crypto/storage/spi/interface_test.go index 7747afd91c..8671d6ada7 100644 --- a/crypto/storage/spi/interface_test.go +++ b/crypto/storage/spi/interface_test.go @@ -22,7 +22,7 @@ import ( "crypto/elliptic" "crypto/rand" "errors" - "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v3/jwk" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" "testing" @@ -38,13 +38,13 @@ func TestPublicKeyEntry_UnmarshalJSON(t *testing.T) { t.Run("error - invalid publicKeyJwk format", func(t *testing.T) { err := (&PublicKeyEntry{}).UnmarshalJSON([]byte("{\"publicKeyJwk\":{}}")) - assert.EqualError(t, err, "could not parse publicKeyEntry: invalid publickeyJwk: invalid key type from JSON ()") + assert.EqualError(t, err, "could not parse publicKeyEntry: invalid publickeyJwk: jwk.ParseKey: invalid key type from JSON ()") }) } func TestPublicKeyEntry_FromJWK(t *testing.T) { privateKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - pk, _ := jwk.FromRaw(privateKey) + pk, _ := jwk.Import(privateKey) entry := PublicKeyEntry{} err := entry.FromJWK(pk) diff --git a/crypto/storage/vault/vault_test.go b/crypto/storage/vault/vault_test.go index cf7db0ea9f..57b9095d03 100644 --- a/crypto/storage/vault/vault_test.go +++ b/crypto/storage/vault/vault_test.go @@ -292,7 +292,6 @@ func TestNewVaultKVStorage(t *testing.T) { t.Run("error - wrong URL", func(t *testing.T) { storage, err := NewVaultKVStorage(Config{Address: "http://non-existing"}) require.Error(t, err) - assert.Regexp(t, `no such host|Temporary failure in name resolution`, err.Error()) assert.Nil(t, storage) }) } diff --git a/didman/api/v1/client_test.go b/didman/api/v1/client_test.go index db74049406..3520f291a5 100644 --- a/didman/api/v1/client_test.go +++ b/didman/api/v1/client_test.go @@ -115,7 +115,7 @@ func TestHTTPClient_AddEndpoint(t *testing.T) { Address: "not_an_address", Timeout: time.Second}, } endpoint, err := c.AddEndpoint("abc", "type", "some-url") - assert.Regexp(t, `no such host|Temporary failure in name resolution`, err.Error()) + assert.Error(t, err) assert.Nil(t, endpoint) }) } @@ -139,7 +139,7 @@ func TestHTTPClient_DeleteEndpointsByType(t *testing.T) { Address: "not_an_address", Timeout: time.Second}, } err := c.DeleteEndpointsByType("did:nuts:123", "eOverdracht") - assert.Regexp(t, `no such host|Temporary failure in name resolution`, err.Error()) + assert.Error(t, err) }) } @@ -228,7 +228,7 @@ func TestHTTPClient_GetCompoundServices(t *testing.T) { Address: "not_an_address", Timeout: time.Second}, } res, err := c.GetCompoundServices("did:nuts:123") - assert.Regexp(t, `no such host|Temporary failure in name resolution`, err.Error()) + assert.Error(t, err) assert.Nil(t, res) }) diff --git a/discovery/client_test.go b/discovery/client_test.go index cbebab6add..3322c0876e 100644 --- a/discovery/client_test.go +++ b/discovery/client_test.go @@ -21,7 +21,7 @@ package discovery import ( "context" "errors" - "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/lestrrat-go/jwx/v3/jwt" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/go-did/vc" "github.com/nuts-foundation/nuts-node/audit" diff --git a/discovery/module.go b/discovery/module.go index bba9412042..ef8174af80 100644 --- a/discovery/module.go +++ b/discovery/module.go @@ -236,10 +236,11 @@ func (m *Module) verifyRegistration(definition ServiceDefinition, presentation v return errors.Join(ErrInvalidPresentation, errPresentationWithoutID) } // Make sure the presentation is intended for this service - if err := validateAudience(definition, presentation.JWT().Audience()); err != nil { + audience, _ := presentation.JWT().Audience() + if err := validateAudience(definition, audience); err != nil { return err } - expiration := presentation.JWT().Expiration() + expiration, _ := presentation.JWT().Expiration() if expiration.IsZero() { return errors.Join(ErrInvalidPresentation, errPresentationWithoutExpiration) } @@ -275,7 +276,7 @@ func (m *Module) verifyRegistration(definition ServiceDefinition, presentation v func (m *Module) validateRegistration(definition ServiceDefinition, presentation vc.VerifiablePresentation) error { // VP can't be valid longer than the credentialRecord it contains - expiration := presentation.JWT().Expiration() + expiration, _ := presentation.JWT().Expiration() for _, cred := range presentation.VerifiableCredential { if cred.ExpirationDate != nil && expiration.After(*cred.ExpirationDate) { return errPresentationValidityExceedsCredentials @@ -306,9 +307,8 @@ func (m *Module) validateRetraction(serviceID string, presentation vc.Verifiable // RFC022 §3.4: it MUST contain a retract_jti JWT claim, containing the jti of the presentation to retract. // Check that the retraction refers to an existing presentation. // If not, it might've already been removed due to expiry or superseded by a newer presentation. - retractJTIRaw, _ := presentation.JWT().Get("retract_jti") - retractJTI, ok := retractJTIRaw.(string) - if !ok || retractJTI == "" { + var retractJTI string + if err := presentation.JWT().Get("retract_jti", &retractJTI); err != nil || retractJTI == "" { return errInvalidRetractionJTIClaim } signerDID, _ := credential.PresentationSigner(presentation) // checked before @@ -317,7 +317,7 @@ func (m *Module) validateRetraction(serviceID string, presentation vc.Verifiable return err } if !exists { - if _, ok = m.serverDefinitions[serviceID]; ok { // only throw an error if acting as server for this service, see https://github.com/nuts-foundation/nuts-node/issues/3691 + if _, ok := m.serverDefinitions[serviceID]; ok { // only throw an error if acting as server for this service, see https://github.com/nuts-foundation/nuts-node/issues/3691 return errRetractionReferencesUnknownPresentation } log.Logger().Warnf("Ignored retraction (ID=%s) signed by (did=%s) for (service=%s) that references a VP (retractedJTI=%s) that does not exist (anymore).", presentation.ID, signerDID.String(), serviceID, retractJTI) diff --git a/discovery/module_test.go b/discovery/module_test.go index 56a0a031cb..dfec97aea1 100644 --- a/discovery/module_test.go +++ b/discovery/module_test.go @@ -22,7 +22,7 @@ import ( "context" "encoding/json" "errors" - "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/lestrrat-go/jwx/v3/jwt" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/go-did/vc" "github.com/nuts-foundation/nuts-node/core" @@ -483,6 +483,27 @@ func TestModule_Search(t *testing.T) { actualJSON, _ := json.Marshal(results) assert.JSONEq(t, string(expectedJSON), string(actualJSON)) }) + t.Run("retracted presentations are not returned", func(t *testing.T) { + resetStore(t, storageEngine.GetSQLDatabase()) + m, testContext := setupModule(t, storageEngine, func(module *Module) { + module.config.Client.RefreshInterval = 0 + }) + testContext.verifier.EXPECT().VerifyVP(gomock.Any(), true, true, nil).Times(2) + + vpAliceRetract := createPresentationCustom(aliceDID, func(claims map[string]interface{}, vp *vc.VerifiablePresentation) { + vp.Type = append(vp.Type, retractionPresentationType) + claims["retract_jti"] = vpAlice.ID.String() + claims[jwt.AudienceKey] = []string{testServiceID} + }) + + require.NoError(t, m.Register(context.Background(), testServiceID, vpAlice)) + require.NoError(t, m.Register(context.Background(), testServiceID, vpAliceRetract)) + + // Empty query: no credential-join filter, so retraction markers leak through. + results, err := m.Search(testServiceID, map[string]string{}) + require.NoError(t, err) + assert.Empty(t, results, "retraction presentations must not be returned from Search") + }) t.Run("unknown service ID", func(t *testing.T) { m, _ := setupModule(t, storageEngine) _, err := m.Search("unknown", nil) diff --git a/discovery/store.go b/discovery/store.go index 5dfba82986..e440a082a0 100644 --- a/discovery/store.go +++ b/discovery/store.go @@ -210,6 +210,7 @@ func storePresentation(tx *gorm.DB, serviceID string, timestamp int, presentatio return nil, err } + presentationExpiration, _ := presentation.JWT().Expiration() newPresentation := presentationRecord{ ID: uuid.NewString(), ServiceID: serviceID, @@ -217,7 +218,7 @@ func storePresentation(tx *gorm.DB, serviceID string, timestamp int, presentatio LamportTimestamp: timestamp, PresentationID: presentation.ID.String(), PresentationRaw: presentation.Raw(), - PresentationExpiration: presentation.JWT().Expiration().Unix(), + PresentationExpiration: presentationExpiration.Unix(), } credentialStore := store.CredentialStore{} @@ -292,6 +293,10 @@ func (s *sqlStore) search(serviceID string, query map[string]string, allowUnvali if err != nil { return nil, fmt.Errorf("failed to parse presentation '%s': %w", match.PresentationID, err) } + // Retraction markers are stored on the timeline (for Get()) but must not surface in search results. + if presentation.IsType(retractionPresentationType) { + continue + } results = append(results, *presentation) } return results, nil diff --git a/discovery/store_test.go b/discovery/store_test.go index d1b73ef23b..8239aa0a80 100644 --- a/discovery/store_test.go +++ b/discovery/store_test.go @@ -19,7 +19,7 @@ package discovery import ( - "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/lestrrat-go/jwx/v3/jwt" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/go-did/vc" "github.com/nuts-foundation/nuts-node/core/to" diff --git a/discovery/test.go b/discovery/test.go index 15d913939e..f016014862 100644 --- a/discovery/test.go +++ b/discovery/test.go @@ -26,10 +26,10 @@ import ( "encoding/json" "fmt" "github.com/google/uuid" - "github.com/lestrrat-go/jwx/v2/jwa" - "github.com/lestrrat-go/jwx/v2/jwk" - "github.com/lestrrat-go/jwx/v2/jws" - "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/lestrrat-go/jwx/v3/jwa" + "github.com/lestrrat-go/jwx/v3/jwk" + "github.com/lestrrat-go/jwx/v3/jws" + "github.com/lestrrat-go/jwx/v3/jwt" ssi "github.com/nuts-foundation/go-did" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/go-did/vc" @@ -264,13 +264,13 @@ func signJWT(subjectDID did.DID, claims map[string]interface{}, headers map[stri if signingKey == nil { return "", fmt.Errorf("key not found for DID: %s", subjectDID) } - subjectKeyJWK, err := jwk.FromRaw(signingKey) + subjectKeyJWK, err := jwk.Import(signingKey) if err != nil { return "", nil } keyID := did.DIDURL{DID: subjectDID} keyID.Fragment = "0" - if err := subjectKeyJWK.Set(jwk.AlgorithmKey, jwa.ES256); err != nil { + if err := subjectKeyJWK.Set(jwk.AlgorithmKey, jwa.ES256()); err != nil { return "", err } if err := subjectKeyJWK.Set(jwk.KeyIDKey, keyID.String()); err != nil { @@ -290,7 +290,8 @@ func signJWT(subjectDID did.DID, claims map[string]interface{}, headers map[stri return "", err } } - bytes, err := jwt.Sign(token, jwt.WithKey(subjectKeyJWK.Algorithm(), subjectKeyJWK, jws.WithProtectedHeaders(hdr))) + alg, _ := subjectKeyJWK.Algorithm() + bytes, err := jwt.Sign(token, jwt.WithKey(alg, subjectKeyJWK, jws.WithProtectedHeaders(hdr))) return string(bytes), err } diff --git a/docs/pages/release_notes.rst b/docs/pages/release_notes.rst index adc40ae42b..5223c0b9d9 100644 --- a/docs/pages/release_notes.rst +++ b/docs/pages/release_notes.rst @@ -2,6 +2,110 @@ Release notes ############# +***************** +Peanut (v6.2.10) +***************** + +Release date: 2026-07-09 + +- Upgrade Go to 1.26.5 to address `GO-2026-5856 `_ (de-anonymization of Encrypted Client Hello (ECH) handshakes: PSK identities were disclosed in the unencrypted client hello, allowing passive network observers to de-anonymize sessions). + +**Full Changelog**: https://github.com/nuts-foundation/nuts-node/compare/v6.2.9...v6.2.10 + +**************** +Peanut (v6.2.9) +**************** + +Release date: 2026-06-30 + +- Upgrade ``github.com/jackc/pgx/v5`` to v5.9.2 to address `GO-2026-5004 `_ (SQL injection in the non-default simple protocol when a dollar-quoted string literal contains an attacker-controlled value that looks like a placeholder). + +**Full Changelog**: https://github.com/nuts-foundation/nuts-node/compare/v6.2.8...v6.2.9 + +**************** +Peanut (v6.2.8) +**************** + +Release date: 2026-06-04 + +- Upgrade Go to 1.26.4 to address `GO-2026-5037 `_ (quadratic-time denial of service in ``crypto/x509`` ``VerifyHostname`` when verifying certificates with large DNS SAN lists) and `GO-2026-5039 `_ (error message injection in ``net/textproto`` where user input is included unescaped in error messages). + +**Full Changelog**: https://github.com/nuts-foundation/nuts-node/compare/v6.2.7...v6.2.8 + +**************** +Peanut (v6.2.7) +**************** + +Release date: 2026-05-27 + +- Upgrade ``golang.org/x/crypto`` to v0.52.0 and ``golang.org/x/net`` to v0.55.0 to address `GO-2026-5018 `_ (DoS via unbounded RSA/DSA key sizes in ``golang.org/x/crypto/ssh`` public key parsers) and `GO-2026-5026 `_ (privilege escalation in ``golang.org/x/net/idna`` where ``ToASCII``/``ToUnicode`` accept Punycode-encoded labels that decode to ASCII-only labels). +- Upgrade ``github.com/go-jose/go-jose/v4`` to v4.1.4 to address `GO-2026-4945 `_ (panic during JWE decryption of crafted tokens). +- Upgrade OpenTelemetry OTLP HTTP exporters (``otlplog/otlploghttp`` to v0.19.0, ``otlptrace/otlptracehttp`` to v1.43.0) to address `GO-2026-4985 `_ (memory exhaustion from oversized OTLP HTTP response bodies). + +**Full Changelog**: https://github.com/nuts-foundation/nuts-node/compare/v6.2.6...v6.2.7 + +**************** +Peanut (v6.2.6) +**************** + +Release date: 2026-05-18 + +- Exclude retraction presentations from Discovery Service search results (backport of `#4193 `_). +- Make HTTP error handler idempotent on already-committed responses to prevent double-write attempts (backport of `#4243 `_). + +**Full Changelog**: https://github.com/nuts-foundation/nuts-node/compare/v6.2.5...v6.2.6 + +**************** +Peanut (v6.2.5) +**************** + +Release date: 2026-05-11 + +- Update Alpine base image to 3.23.4 to pick up musl, OpenSSL and zlib security fixes. +- Upgrade Go to 1.26.3 and ``golang.org/x/net`` to v0.53.0 to address `GO-2026-4986 `_, `GO-2026-4982 `_, `GO-2026-4980 `_, `GO-2026-4977 `_, `GO-2026-4971 `_ and `GO-2026-4918 `_ (XSS in ``html/template``, quadratic concatenation in ``net/mail``, panic in ``net`` on Windows NUL byte, infinite loop in HTTP/2 transport). + +**Full Changelog**: https://github.com/nuts-foundation/nuts-node/compare/v6.2.4...v6.2.5 + +**************** +Peanut (v6.2.4) +**************** + +Release date: 2026-04-16 + +- Fix node failing to start when syncing many transactions on a slow disk (e.g. SMB/Azure Files): concurrent read transactions were incorrectly blocked by pending write transactions at the Go level, causing read lock timeouts during startup. See `#4162 `_. + +**Full Changelog**: https://github.com/nuts-foundation/nuts-node/compare/v6.2.3...v6.2.4 + +**************** +Peanut (v6.2.3) +**************** + +Release date: 2026-04-14 + +- Tighten validation of access tokens in the v1 introspection endpoint: require ``typ`` header to be ``at+jwt``, require non-empty ``iss``, ``sub`` and ``service`` claims, and verify that the ``iss`` claim matches the DID of the signing key. + +**Full Changelog**: https://github.com/nuts-foundation/nuts-node/compare/v6.2.2...v6.2.3 + +**************** +Peanut (v6.2.2) +**************** + +Release date: 2026-04-10 + +- Upgrade Go to 1.26.2 to fix GO-2026-4865 (html/template XSS), GO-2026-4866 (crypto/x509 auth bypass), GO-2026-4869 (archive/tar DoS), GO-2026-4870 (crypto/tls DoS), GO-2026-4946 (crypto/x509 DoS) + +**Full Changelog**: https://github.com/nuts-foundation/nuts-node/compare/v6.2.1...v6.2.2 + +**************** +Peanut (v6.2.1) +**************** + +Release date: 2026-03-25 + +- Update NATS server to fix CVE-2026-33215, CVE-2026-33216, CVE-2026-33217, CVE-2026-33218, CVE-2026-33219, CVE-2026-33222, CVE-2026-33223, CVE-2026-33246, CVE-2026-33247, CVE-2026-33248, CVE-2026-33249 + +**Full Changelog**: https://github.com/nuts-foundation/nuts-node/compare/v6.2.0...v6.2.1 + **************** Peanut (v6.2.0) **************** diff --git a/e2e-tests/storage/vault/docker-compose.yml b/e2e-tests/storage/vault/docker-compose.yml index 636f6fda0a..0084b19afb 100644 --- a/e2e-tests/storage/vault/docker-compose.yml +++ b/e2e-tests/storage/vault/docker-compose.yml @@ -9,6 +9,9 @@ services: - "./nuts.yaml:/opt/nuts/nuts.yaml:ro" - "../../tls-certs/nodeA-certificate.pem:/opt/nuts/certificate-and-key.pem:ro" - "../../tls-certs/truststore.pem:/opt/nuts/truststore.pem:ro" + depends_on: + vault-adapter: + condition: service_healthy healthcheck: interval: 1s # Make test run quicker by checking health status more often vault-adapter: @@ -16,9 +19,31 @@ services: environment: VAULT_ADDR: http://vault:8200 VAULT_TOKEN: root + depends_on: + vault: + condition: service_healthy + healthcheck: + # The image already defines a HEALTHCHECK using curl against /health. + # Override only the interval to make the e2e test start faster. + interval: 1s + timeout: 2s + retries: 30 vault: image: hashicorp/vault cap_add: - IPC_LOCK environment: - VAULT_DEV_ROOT_TOKEN_ID: root \ No newline at end of file + VAULT_DEV_ROOT_TOKEN_ID: root + VAULT_ADDR: http://127.0.0.1:8200 + # Skip setcap on the vault binary at container startup. Some CI runners + # don't grant CAP_SETFCAP, which causes vault's entrypoint to fail with + # "unable to set CAP_SETFCAP effective capability: Operation not permitted". + # mlock isn't required for dev mode. + SKIP_SETCAP: "true" + healthcheck: + # vault CLI returns exit 0 when the API is reachable and vault is unsealed + # (dev mode is always unsealed), exit 1/2 otherwise. + test: ["CMD", "vault", "status"] + interval: 1s + timeout: 2s + retries: 30 \ No newline at end of file diff --git a/e2e-tests/storage/vault/run-test.sh b/e2e-tests/storage/vault/run-test.sh index fae1fe2637..dfa74768b5 100755 --- a/e2e-tests/storage/vault/run-test.sh +++ b/e2e-tests/storage/vault/run-test.sh @@ -11,7 +11,8 @@ docker compose rm -f -v echo "------------------------------------" echo "Setting up Vault..." echo "------------------------------------" -docker compose up --wait vault && sleep 2 +# Wait for vault-adapter to become healthy; this transitively waits for vault to be reachable. +docker compose up --wait vault-adapter docker compose exec -e VAULT_TOKEN=root vault vault secrets enable -version=1 -address=http://localhost:8200 kv echo "------------------------------------" diff --git a/go.mod b/go.mod index 5e59cd817b..d442dbc6c9 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module github.com/nuts-foundation/nuts-node // This is the minimal version, the actual go version is determined by the images in the Dockerfile // This version is used in automated tests such as the 'Scheduled govulncheck' action -go 1.26.1 +go 1.26.5 require ( github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 @@ -24,16 +24,15 @@ require ( github.com/knadh/koanf/providers/structs v1.0.0 github.com/knadh/koanf/v2 v2.3.2 github.com/labstack/echo/v4 v4.15.0 - github.com/lestrrat-go/jwx/v2 v2.1.6 github.com/mdp/qrterminal/v3 v3.2.1 - github.com/mr-tron/base58 v1.2.0 + github.com/mr-tron/base58 v1.3.0 github.com/multiformats/go-multicodec v0.10.0 - github.com/nats-io/nats-server/v2 v2.11.12 + github.com/nats-io/nats-server/v2 v2.11.15 github.com/nats-io/nats.go v1.49.0 github.com/nuts-foundation/crypto-ecies v0.0.0-20211207143025-5b84f9efce2b - github.com/nuts-foundation/go-did v0.17.0 + github.com/nuts-foundation/go-did v0.22.0 github.com/nuts-foundation/go-leia/v4 v4.2.0 - github.com/nuts-foundation/go-stoabs v1.11.0 + github.com/nuts-foundation/go-stoabs v1.11.1 github.com/nuts-foundation/sqlite v1.0.0 // check the oapi-codegen tool version in the makefile when upgrading the runtime github.com/oapi-codegen/runtime v1.1.2 @@ -53,9 +52,9 @@ require ( go.uber.org/atomic v1.11.0 go.uber.org/goleak v1.3.0 go.uber.org/mock v0.6.0 - golang.org/x/crypto v0.48.0 - golang.org/x/time v0.14.0 - google.golang.org/grpc v1.79.3 + golang.org/x/crypto v0.53.0 + golang.org/x/time v0.15.0 + google.golang.org/grpc v1.80.0 google.golang.org/protobuf v1.36.11 gopkg.in/Regis24GmbH/go-phonetics.v2 v2.0.3 gopkg.in/yaml.v3 v3.0.1 @@ -86,7 +85,7 @@ require ( github.com/chromedp/cdproto v0.0.0-20250724212937-08a3db8b4327 // indirect github.com/chromedp/sysutil v1.1.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/edsrzf/mmap-go v1.1.0 // indirect @@ -97,7 +96,7 @@ require ( github.com/go-chi/chi/v5 v5.2.2 // indirect github.com/go-co-op/gocron v1.28.3 // indirect github.com/go-errors/errors v1.4.2 // indirect - github.com/go-jose/go-jose/v4 v4.1.3 // indirect + github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/go-redis/redis/v8 v8.11.5 // indirect github.com/go-redsync/redsync/v4 v4.13.0 // indirect github.com/go-sql-driver/mysql v1.9.3 // indirect @@ -105,7 +104,7 @@ require ( github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/ws v1.4.0 // indirect - github.com/goccy/go-json v0.10.5 // indirect + github.com/goccy/go-json v0.10.6 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/golang-jwt/jwt/v5 v5.3.1 // indirect github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect @@ -122,19 +121,20 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/jackc/pgx/v5 v5.7.5 + github.com/jackc/pgx/v5 v5.9.2 github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect - github.com/klauspost/compress v1.18.3 // indirect + github.com/klauspost/compress v1.18.4 // indirect github.com/knadh/koanf/maps v0.1.2 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/labstack/gommon v0.4.2 // indirect - github.com/lestrrat-go/blackmagic v1.0.3 // indirect + github.com/lestrrat-go/blackmagic v1.0.4 // indirect + github.com/lestrrat-go/dsig v1.2.1 // indirect + github.com/lestrrat-go/dsig-secp256k1 v1.0.0 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect - github.com/lestrrat-go/httprc v1.0.6 // indirect - github.com/lestrrat-go/iter v1.0.2 // indirect - github.com/lestrrat-go/option v1.0.1 // indirect + github.com/lestrrat-go/httprc/v3 v3.0.5 // indirect + github.com/lestrrat-go/option/v2 v2.0.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mfridman/interpolate v0.0.2 // indirect @@ -149,11 +149,11 @@ require ( github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/multiformats/go-base32 v0.1.0 // indirect github.com/multiformats/go-base36 v0.2.0 // indirect - github.com/multiformats/go-multibase v0.2.0 // indirect + github.com/multiformats/go-multibase v0.3.0 // indirect github.com/multiformats/go-multihash v0.0.11 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/nats-io/jwt/v2 v2.8.0 // indirect - github.com/nats-io/nkeys v0.4.12 // indirect + github.com/nats-io/jwt/v2 v2.8.1 // indirect + github.com/nats-io/nkeys v0.4.15 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect github.com/nightlyone/lockfile v1.0.0 // indirect @@ -166,7 +166,7 @@ require ( github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/robfig/cron/v3 v3.0.1 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect - github.com/segmentio/asm v1.2.0 // indirect + github.com/segmentio/asm v1.2.1 // indirect github.com/sethvargo/go-retry v0.3.0 // indirect github.com/shengdoushi/base58 v1.0.0 // indirect github.com/shopspring/decimal v1.4.0 // indirect @@ -184,12 +184,12 @@ require ( github.com/x448/float16 v0.8.4 // indirect github.com/yuin/gopher-lua v1.1.1 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/net v0.51.0 // indirect - golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.42.0 // indirect - golang.org/x/term v0.40.0 // indirect - golang.org/x/text v0.34.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect + golang.org/x/net v0.55.0 // indirect + golang.org/x/sync v0.21.0 // indirect + golang.org/x/sys v0.46.0 // indirect + golang.org/x/term v0.44.0 // indirect + golang.org/x/text v0.38.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect gopkg.in/Regis24GmbH/go-diacritics.v2 v2.0.3 // indirect gorm.io/gorm v1.31.1 modernc.org/mathutil v1.7.1 // indirect @@ -208,21 +208,22 @@ require ( github.com/eko/gocache/store/go_cache/v4 v4.2.2 github.com/eko/gocache/store/memcache/v4 v4.2.2 github.com/eko/gocache/store/redis/v4 v4.2.2 + github.com/lestrrat-go/jwx/v3 v3.1.1 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/uptrace/opentelemetry-go-extra/otelgorm v0.3.2 go.opentelemetry.io/contrib/bridges/otellogrus v0.17.0 go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho v0.65.0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.66.0 - go.opentelemetry.io/otel v1.42.0 - go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0 - go.opentelemetry.io/otel/sdk v1.42.0 - go.opentelemetry.io/otel/sdk/log v0.18.0 - go.opentelemetry.io/otel/trace v1.42.0 + go.opentelemetry.io/otel v1.43.0 + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 + go.opentelemetry.io/otel/sdk v1.43.0 + go.opentelemetry.io/otel/sdk/log v0.19.0 + go.opentelemetry.io/otel/trace v1.43.0 ) require ( - github.com/antithesishq/antithesis-sdk-go v0.5.0-default-no-op // indirect + github.com/antithesishq/antithesis-sdk-go v0.6.0-default-no-op // indirect github.com/aws/aws-sdk-go-v2/credentials v1.19.7 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 // indirect @@ -248,14 +249,15 @@ require ( github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/rs/zerolog v1.26.1 // indirect github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 // indirect + github.com/valyala/fastjson v1.6.10 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 // indirect - go.opentelemetry.io/otel/log v0.18.0 // indirect - go.opentelemetry.io/otel/metric v1.42.0 // indirect - go.opentelemetry.io/proto/otlp v1.9.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect + go.opentelemetry.io/otel/log v0.19.0 // indirect + go.opentelemetry.io/otel/metric v1.43.0 // indirect + go.opentelemetry.io/proto/otlp v1.10.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect modernc.org/libc v1.70.0 // indirect ) diff --git a/go.sum b/go.sum index 9733fa549a..d3ab1a2601 100644 --- a/go.sum +++ b/go.sum @@ -41,8 +41,8 @@ github.com/alexandrevicenzi/go-sse v1.6.0/go.mod h1:jdrNAhMgVqP7OfcUuM8eJx0sOY17 github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI= github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM= github.com/alvaroloes/enumer v1.1.2/go.mod h1:FxrjvuXoDAx9isTJrv4c+T410zFi0DtXIT0m65DJ+Wo= -github.com/antithesishq/antithesis-sdk-go v0.5.0-default-no-op h1:Ucf+QxEKMbPogRO5guBNe5cgd9uZgfoJLOYs8WWhtjM= -github.com/antithesishq/antithesis-sdk-go v0.5.0-default-no-op/go.mod h1:IUpT2DPAKh6i/YhSbt6Gl3v2yvUZjmKncl7U91fup7E= +github.com/antithesishq/antithesis-sdk-go v0.6.0-default-no-op h1:kpBdlEPbRvff0mDD1gk7o9BhI16b9p5yYAXRlidpqJE= +github.com/antithesishq/antithesis-sdk-go v0.6.0-default-no-op/go.mod h1:IUpT2DPAKh6i/YhSbt6Gl3v2yvUZjmKncl7U91fup7E= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= github.com/avast/retry-go/v4 v4.7.0 h1:yjDs35SlGvKwRNSykujfjdMxMhMQQM0TnIjJaHB+Zio= @@ -125,8 +125,8 @@ github.com/daangn/minimemcached v1.2.0/go.mod h1:ewcvvKcPuzp5tQjELLUXDZJtb3L1Uqx github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/dgraph-io/badger/v4 v4.6.0 h1:acOwfOOZ4p1dPRnYzvkVm7rUk2Y21TgPVepCy5dJdFQ= github.com/dgraph-io/badger/v4 v4.6.0/go.mod h1:KSJ5VTuZNC3Sd+YhvVjk2nYua9UZnnTr/SkXvdtiPgI= github.com/dgraph-io/ristretto/v2 v2.1.0 h1:59LjpOJLNDULHh8MC4UaegN52lC4JnO2dITsie/Pa8I= @@ -168,8 +168,8 @@ github.com/go-co-op/gocron v1.28.3 h1:swTsge6u/1Ei51b9VLMz/YTzEzWpbsk5SiR7m5fklT github.com/go-co-op/gocron v1.28.3/go.mod h1:39f6KNSGVOU1LO/ZOoZfcSxwlsJDQOKSu8erN0SH48Y= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= -github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= -github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= +github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2 h1:iizUGZ9pEquQS5jTGkh4AqeeHCMbfbjeb0zMt0aEFzs= github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -199,8 +199,8 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= -github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= -github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= +github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= @@ -271,8 +271,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs= -github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= +github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw= +github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= @@ -288,8 +288,8 @@ github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/ github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= -github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw= -github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= +github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= @@ -323,18 +323,20 @@ github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0 github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo= github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= -github.com/lestrrat-go/blackmagic v1.0.3 h1:94HXkVLxkZO9vJI/w2u1T0DAoprShFd13xtnSINtDWs= -github.com/lestrrat-go/blackmagic v1.0.3/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw= +github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA= +github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw= +github.com/lestrrat-go/dsig v1.2.1 h1:MwxzZhE4+4fguHi+uDALKVlC3Cn+O1QU1Q/F8D7hVIc= +github.com/lestrrat-go/dsig v1.2.1/go.mod h1:RD2eOaidyPvpc7IJQoO3Qq52RWdy8ZcJs8lrOnoa1Kc= +github.com/lestrrat-go/dsig-secp256k1 v1.0.0 h1:JpDe4Aybfl0soBvoVwjqDbp+9S1Y2OM7gcrVVMFPOzY= +github.com/lestrrat-go/dsig-secp256k1 v1.0.0/go.mod h1:CxUgAhssb8FToqbL8NjSPoGQlnO4w3LG1P0qPWQm/NU= 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.6 h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCGW8k= -github.com/lestrrat-go/httprc v1.0.6/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= -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/jwx/v2 v2.1.6 h1:hxM1gfDILk/l5ylers6BX/Eq1m/pnxe9NBwW6lVfecA= -github.com/lestrrat-go/jwx/v2 v2.1.6/go.mod h1:Y722kU5r/8mV7fYDifjug0r8FK8mZdw0K0GpJw/l8pU= -github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= -github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lestrrat-go/httprc/v3 v3.0.5 h1:S+Mb4L2I+bM6JGTibLmxExhyTOqnXjqx+zi9MoXw/TM= +github.com/lestrrat-go/httprc/v3 v3.0.5/go.mod h1:mSMtkZW92Z98M5YoNNztbRGxbXHql7tSitCvaxvo9l0= +github.com/lestrrat-go/jwx/v3 v3.1.1 h1:yd9AdPmZ4INnQ7k42IrzXYpnEG803+SrQ6hdMvzHJzw= +github.com/lestrrat-go/jwx/v3 v3.1.1/go.mod h1:uw/MN2M/Xiu4FhwcIwH11Zsh9JWx9SWzgALl7/uIEkU= +github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss= +github.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -366,28 +368,28 @@ github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/mr-tron/base58 v1.1.3/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= -github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= -github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/mr-tron/base58 v1.3.0 h1:K6Y13R2h+dku0wOqKtecgRnBUBPrZzLZy5aIj8lCcJI= +github.com/mr-tron/base58 v1.3.0/go.mod h1:2BuubE67DCSWwVfx37JWNG8emOC0sHEU4/HpcYgCLX8= github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= -github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= -github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= +github.com/multiformats/go-multibase v0.3.0 h1:8helZD2+4Db7NNWFiktk2NePbF0boolBe6bDQvM4r68= +github.com/multiformats/go-multibase v0.3.0/go.mod h1:MoBLQPCkRTOL3eveIPO81860j2AQY8JwcnNlRkGRUfI= github.com/multiformats/go-multicodec v0.10.0 h1:UpP223cig/Cx8J76jWt91njpK3GTAO1w02sdcjZDSuc= github.com/multiformats/go-multicodec v0.10.0/go.mod h1:wg88pM+s2kZJEQfRCKBNU+g32F5aWBEjyFHXvZLTcLI= github.com/multiformats/go-multihash v0.0.11 h1:yEyBxwoR/7vBM5NfLVXRnpQNVLrMhpS6MRb7Z/1pnzc= github.com/multiformats/go-multihash v0.0.11/go.mod h1:LXRDJcYYY+9BjlsFe6i5LV7uekf0OoEJdnRmitUshxk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/nats-io/jwt/v2 v2.8.0 h1:K7uzyz50+yGZDO5o772eRE7atlcSEENpL7P+b74JV1g= -github.com/nats-io/jwt/v2 v2.8.0/go.mod h1:me11pOkwObtcBNR8AiMrUbtVOUGkqYjMQZ6jnSdVUIA= -github.com/nats-io/nats-server/v2 v2.11.12 h1:jGDXTkcjqQ5fCRstwIxvv1K0RHfftFUoSCT/iIZcqOc= -github.com/nats-io/nats-server/v2 v2.11.12/go.mod h1:5MCp/pqm5SEfsvVZ31ll1088ZTwEUdvRX1Hmh/mTTDg= +github.com/nats-io/jwt/v2 v2.8.1 h1:V0xpGuD/N8Mi+fQNDynXohVvp7ZztevW5io8CUWlPmU= +github.com/nats-io/jwt/v2 v2.8.1/go.mod h1:nWnOEEiVMiKHQpnAy4eXlizVEtSfzacZ1Q43LIRavZg= +github.com/nats-io/nats-server/v2 v2.11.15 h1:StSf9TINInaZtr4oww2+kXmfwa9SkN//g/LwS19/UJ0= +github.com/nats-io/nats-server/v2 v2.11.15/go.mod h1:zwhv8Y0PE3KHyKgznJc/9Xoai638SaJd83zzJ5GJn74= github.com/nats-io/nats.go v1.49.0 h1:yh/WvY59gXqYpgl33ZI+XoVPKyut/IcEaqtsiuTJpoE= github.com/nats-io/nats.go v1.49.0/go.mod h1:fDCn3mN5cY8HooHwE2ukiLb4p4G4ImmzvXyJt+tGwdw= -github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc= -github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg= +github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4= +github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= @@ -396,12 +398,12 @@ github.com/nightlyone/lockfile v1.0.0 h1:RHep2cFKK4PonZJDdEl4GmkabuhbsRMgk/k3uAm github.com/nightlyone/lockfile v1.0.0/go.mod h1:rywoIealpdNse2r832aiD9jRk8ErCatROs6LzC841CI= github.com/nuts-foundation/crypto-ecies v0.0.0-20211207143025-5b84f9efce2b h1:80icUxWHwE1MrIOOEK5rxrtyKOgZeq5Iu1IjAEkggTY= github.com/nuts-foundation/crypto-ecies v0.0.0-20211207143025-5b84f9efce2b/go.mod h1:6YUioYirD6/8IahZkoS4Ypc8xbeJW76Xdk1QKcziNTM= -github.com/nuts-foundation/go-did v0.17.0 h1:nLmMiiKjIJwgZsfJ98ywATiCb9VHomnb3r86oWHdILw= -github.com/nuts-foundation/go-did v0.17.0/go.mod h1:8VLZhVjkFH9VgGu//3y7ICowwItpym3NWkOih1Ka1fw= +github.com/nuts-foundation/go-did v0.22.0 h1:cjGbLv8S+MMlwlnuy9YD6wdBWDKguweCIem8sZ6neQw= +github.com/nuts-foundation/go-did v0.22.0/go.mod h1:M6d7KaJLf3Ipl+ttRANc207S5MT9Kq5F0so1fPad/I0= github.com/nuts-foundation/go-leia/v4 v4.2.0 h1:o/bgYVCyTgsfgtaKmlrcUaJ2z4NwetERC98SUWwYajM= github.com/nuts-foundation/go-leia/v4 v4.2.0/go.mod h1:Gw6bXqJLOAmHSiXJJYbVoj+Mowp/PoBRywO0ZPsVzA0= -github.com/nuts-foundation/go-stoabs v1.11.0 h1:q18jVruPdFcVhodDrnKuhq/24i0pUC/YXgzJS0glKUU= -github.com/nuts-foundation/go-stoabs v1.11.0/go.mod h1:w2OgcHXO0Jl/vN1nG0ksApaeOaPOY3sSBJLaZz7cCY0= +github.com/nuts-foundation/go-stoabs v1.11.1 h1:ZQOeRKzC1+AfW6Ve5kBJMo+zYhHLBUI4KrLWKYkxoeI= +github.com/nuts-foundation/go-stoabs v1.11.1/go.mod h1:LzD3RhINBVJUpVRV9IM86IXcZoBSRx43PpCWRZGbEfs= github.com/nuts-foundation/sqlite v1.0.0 h1:gLKyVIHZqYfYpEy5Ji6vjNUH8rs0luiY3DWNcOSBBzM= github.com/nuts-foundation/sqlite v1.0.0/go.mod h1:2GHDXCw5Sul9L3h8T5k0+558scm4ol4iXG+wVyYqueI= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= @@ -465,8 +467,8 @@ github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkB github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/santhosh-tekuri/jsonschema v1.2.4 h1:hNhW8e7t+H1vgY+1QeEQpveR6D4+OwKPXCfD2aieJis= github.com/santhosh-tekuri/jsonschema v1.2.4/go.mod h1:TEAUOeZSmIxTTuHatJzrvARHiuO9LYd+cIxzgEHCQI4= -github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= -github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= +github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= +github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= github.com/shengdoushi/base58 v1.0.0 h1:tGe4o6TmdXFJWoI31VoSWvuaKxf0Px3gqa3sUWhAxBs= @@ -529,6 +531,8 @@ github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2/go.mod h1:O8bHQfyinKwTX github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADTh4= +github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg= @@ -553,32 +557,32 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.66.0 h1:PnV4kVn go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.66.0/go.mod h1:ofAwF4uinaf8SXdVzzbL4OsxJ3VfeEg3f/F6CeF49/Y= go.opentelemetry.io/contrib/propagators/b3 v1.40.0 h1:xariChe8OOVF3rNlfzGFgQc61npQmXhzZj/i82mxMfg= go.opentelemetry.io/contrib/propagators/b3 v1.40.0/go.mod h1:72WvbdxbOfXaELEQfonFfOL6osvcVjI7uJEE8C2nkrs= -go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= -go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0 h1:djrxvDxAe44mJUrKataUbOhCKhR3F8QCyWucO16hTQs= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0/go.mod h1:dt3nxpQEiSoKvfTVxp3TUg5fHPLhKtbcnN3Z1I1ePD0= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 h1:THuZiwpQZuHPul65w4WcwEnkX2QIuMT+UFoOrygtoJw= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0/go.mod h1:J2pvYM5NGHofZ2/Ru6zw/TNWnEQp5crgyDeSrYpXkAw= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0 h1:uLXP+3mghfMf7XmV4PkGfFhFKuNWoCvvx5wP/wOXo0o= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0/go.mod h1:v0Tj04armyT59mnURNUJf7RCKcKzq+lgJs6QSjHjaTc= -go.opentelemetry.io/otel/log v0.18.0 h1:XgeQIIBjZZrliksMEbcwMZefoOSMI1hdjiLEiiB0bAg= -go.opentelemetry.io/otel/log v0.18.0/go.mod h1:KEV1kad0NofR3ycsiDH4Yjcoj0+8206I6Ox2QYFSNgI= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 h1:HIBTQ3VO5aupLKjC90JgMqpezVXwFuq6Ryjn0/izoag= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0/go.mod h1:ji9vId85hMxqfvICA0Jt8JqEdrXaAkcpkI9HPXya0ro= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak= +go.opentelemetry.io/otel/log v0.19.0 h1:KUZs/GOsw79TBBMfDWsXS+KZ4g2Ckzksd1ymzsIEbo4= +go.opentelemetry.io/otel/log v0.19.0/go.mod h1:5DQYeGmxVIr4n0/BcJvF4upsraHjg6vudJJpnkL6Ipk= go.opentelemetry.io/otel/log/logtest v0.18.0 h1:2QeyoKJdIgK2LJhG1yn78o/zmpXx1EditeyRDREqVS8= go.opentelemetry.io/otel/log/logtest v0.18.0/go.mod h1:v1vh3PYR9zIa5MK6HwkH2lMrLBg/Y9Of6Qc+krlesX0= -go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= -go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= -go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo= -go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts= -go.opentelemetry.io/otel/sdk/log v0.18.0 h1:n8OyZr7t7otkeTnPTbDNom6rW16TBYGtvyy2Gk6buQw= -go.opentelemetry.io/otel/sdk/log v0.18.0/go.mod h1:C0+wxkTwKpOCZLrlJ3pewPiiQwpzycPI/u6W0Z9fuYk= -go.opentelemetry.io/otel/sdk/log/logtest v0.16.0 h1:/XVkpZ41rVRTP4DfMgYv1nEtNmf65XPPyAdqV90TMy4= -go.opentelemetry.io/otel/sdk/log/logtest v0.16.0/go.mod h1:iOOPgQr5MY9oac/F5W86mXdeyWZGleIx3uXO98X2R6Y= -go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA= -go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc= -go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= -go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= -go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= -go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/log v0.19.0 h1:scYVLqT22D2gqXItnWiocLUKGH9yvkkeql5dBDiXyko= +go.opentelemetry.io/otel/sdk/log v0.19.0/go.mod h1:vFBowwXGLlW9AvpuF7bMgnNI95LiW10szrOdvzBHlAg= +go.opentelemetry.io/otel/sdk/log/logtest v0.19.0 h1:BEbF7ZBB6qQloV/Ub1+3NQoOUnVtcGkU3XX4Ws3GQfk= +go.opentelemetry.io/otel/sdk/log/logtest v0.19.0/go.mod h1:Lua81/3yM0wOmoHTokLj9y9ADeA02v1naRrVrkAZuKk= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= +go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= @@ -609,8 +613,8 @@ golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOM golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= -golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= -golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 h1:MDfG8Cvcqlt9XXrmEiD4epKn7VJHZO84hejP9Jmp0MM= golang.org/x/exp v0.0.0-20251209150349-8475f28825e9/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -620,8 +624,8 @@ golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= -golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= +golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -644,8 +648,8 @@ golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= -golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= -golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -654,8 +658,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -684,8 +688,8 @@ golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -701,8 +705,8 @@ golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= -golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= -golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= +golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= +golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -717,10 +721,10 @@ golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= -golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= -golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190524210228-3d17549cdc6b/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -730,19 +734,19 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= -golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= +golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= -gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0= -google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= -google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= +google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= +google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/Regis24GmbH/go-diacritics.v2 v2.0.3 h1:rz88vn1OH2B9kKorR+QCrcuw6WbizVwahU2Y9Q09xqU= diff --git a/http/engine_test.go b/http/engine_test.go index 31d36d5e11..2ba12c8292 100644 --- a/http/engine_test.go +++ b/http/engine_test.go @@ -32,9 +32,9 @@ import ( "github.com/google/uuid" "github.com/labstack/echo/v4" - "github.com/lestrrat-go/jwx/v2/jwa" - "github.com/lestrrat-go/jwx/v2/jwk" - "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/lestrrat-go/jwx/v3/jwa" + "github.com/lestrrat-go/jwx/v3/jwk" + "github.com/lestrrat-go/jwx/v3/jwt" "github.com/nuts-foundation/nuts-node/core" "github.com/nuts-foundation/nuts-node/http/log" "github.com/nuts-foundation/nuts-node/test" @@ -366,7 +366,7 @@ func generateEd25519TestKey(t *testing.T) (jwk.Key, *jwt.Serializer, []byte) { sshAuthKey := fmt.Sprintf("%v %v random@test.local", sshPub.Type(), b64.StdEncoding.EncodeToString(sshPub.Marshal())) // Convert the base key type to a jwk type - jwkKey, err := jwk.FromRaw(priv) + jwkKey, err := jwk.Import(priv) require.NoError(t, err) // Set the key ID for the jwk to be the public key fingerprint @@ -374,7 +374,7 @@ func generateEd25519TestKey(t *testing.T) (jwk.Key, *jwt.Serializer, []byte) { require.NoError(t, err) // Create a serializer configured to use the generated key - serializer := jwt.NewSerializer().Sign(jwt.WithKey(jwa.EdDSA, jwkKey)) + serializer := jwt.NewSerializer().Sign(jwt.WithKey(jwa.EdDSA(), jwkKey)) t.Logf("authorized_key = %v", sshAuthKey) diff --git a/http/tokenV2/authorized_keys.go b/http/tokenV2/authorized_keys.go index 839d6b2bc9..84c126af50 100644 --- a/http/tokenV2/authorized_keys.go +++ b/http/tokenV2/authorized_keys.go @@ -31,7 +31,7 @@ import ( "github.com/nuts-foundation/nuts-node/http/log" - "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v3/jwk" ) // minimumRSAKeySize defines the minimum length in bits of RSA keys @@ -80,7 +80,7 @@ func jwkFromSSHKey(key ssh.PublicKey) (jwk.Key, error) { } // Use the crypto/* key type to create the jwk key type - converted, err := jwk.FromRaw(cryptoPublicKey) + converted, err := jwk.Import(cryptoPublicKey) if err != nil { return nil, err } diff --git a/http/tokenV2/middleware.go b/http/tokenV2/middleware.go index dbe0bd8846..ab731e5bbb 100644 --- a/http/tokenV2/middleware.go +++ b/http/tokenV2/middleware.go @@ -29,9 +29,9 @@ import ( "github.com/google/uuid" "github.com/labstack/echo/v4" - "github.com/lestrrat-go/jwx/v2/jwa" - "github.com/lestrrat-go/jwx/v2/jws" - "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/lestrrat-go/jwx/v3/jwa" + "github.com/lestrrat-go/jwx/v3/jws" + "github.com/lestrrat-go/jwx/v3/jwt" "github.com/nuts-foundation/nuts-node/audit" "github.com/nuts-foundation/nuts-node/core" "github.com/nuts-foundation/nuts-node/http/log" @@ -162,7 +162,7 @@ func (m middlewareImpl) checkConnectionAuthorization(context echo.Context, next } // Ensure the issuer, the person issuing the JWT, matches the registered username for this key - if authorizedKey.comment != token.Issuer() { + if iss, _ := token.Issuer(); authorizedKey.comment != iss { return unauthorizedError(context, fmt.Errorf("expected issuer (%s) does not match iss", authorizedKey.comment)) } @@ -177,11 +177,14 @@ func (m middlewareImpl) checkConnectionAuthorization(context echo.Context, next // accessGranted allows a connection to be handled func accessGranted(authKey authorizedKey, context echo.Context, token jwt.Token, next echo.HandlerFunc) error { // Create an audit log entry about this access granted event - auditLog := auditLogger(context, token.Subject(), audit.AccessGrantedEvent) - auditLog.Infof("Access granted to user '%v' with JWT %s issued to %s by %s", authKey.comment, token.JwtID(), token.Subject(), token.Issuer()) + sub, _ := token.Subject() + jti, _ := token.JwtID() + iss, _ := token.Issuer() + auditLog := auditLogger(context, sub, audit.AccessGrantedEvent) + auditLog.Infof("Access granted to user '%v' with JWT %s issued to %s by %s", authKey.comment, jti, sub, iss) // Set the username from authorized_keys as the username in the context - context.Set(core.UserContextKey, token.Issuer()) + context.Set(core.UserContextKey, iss) // Call the next handler/middleware, probably serving some content/processing the API request return next(context) @@ -210,28 +213,28 @@ func credentialIsSecure(credential string) error { secureSignatureCount := 0 for _, signature := range message.Signatures() { // Reject credentials signed with insecure algorithms - algorithm := signature.ProtectedHeaders().Algorithm() + algorithm, _ := signature.ProtectedHeaders().Algorithm() if !acceptableSignatureAlgorithm(algorithm) { return fmt.Errorf("signing algorithm %v is not permitted", algorithm) } // Reject credentials trying to provide the public key directly in the header - if signature.ProtectedHeaders().JWK() != nil { + if _, ok := signature.ProtectedHeaders().JWK(); ok { return errors.New("embedding JWK in signature is forbidden") } // Reject credentials trying to provide the public key URL in the header - if signature.ProtectedHeaders().JWKSetURL() != "" { + if jku, ok := signature.ProtectedHeaders().JWKSetURL(); ok && jku != "" { return errors.New("embedding JWKSetURL in signature is forbidden") } // Reject credentials trying to provide an x509 certificate chain in the header - if signature.ProtectedHeaders().X509CertChain() != nil { + if _, ok := signature.ProtectedHeaders().X509CertChain(); ok { return errors.New("embedding X509CertChain in signature is forbidden") } // Reject credentials trying to provide an x509 chain URL in the header - if signature.ProtectedHeaders().X509URL() != "" { + if x5u, ok := signature.ProtectedHeaders().X509URL(); ok && x5u != "" { return errors.New("embedding X509URL in signature is forbidden") } @@ -255,12 +258,10 @@ func mandatoryJWTFields() []string { // tokenJTI returns the JWT's JTI as a string func tokenJTI(token jwt.Token) string { - // Retrieve the JwtID from the token, but it comes out as an interface{} - if jtiIface, ok := token.Get(jwt.JwtIDKey); ok { - // Convert the interface{} to a string so the typed value can be returned - if jtiStr, ok := jtiIface.(string); ok { - return jtiStr - } + // Retrieve the JwtID from the token as a string + var jti string + if err := token.Get(jwt.JwtIDKey, &jti); err == nil { + return jti } // Simply return an empty string if either the jti field wasn't present or it wasn't a string @@ -274,7 +275,8 @@ func tokenJTI(token jwt.Token) string { func bestPracticesCheck(token jwt.Token) error { // Ensure the mandatory fields are present for _, field := range mandatoryJWTFields() { - if _, ok := token.Get(field); !ok { + var v interface{} + if err := token.Get(field, &v); err != nil { return fmt.Errorf("missing field: %v", field) } } @@ -285,25 +287,29 @@ func bestPracticesCheck(token jwt.Token) error { return fmt.Errorf("token jti is not a valid uuid: %w", err) } + nbf, _ := token.NotBefore() + exp, _ := token.Expiration() + iat, _ := token.IssuedAt() + // Ensure the expiration is no more than 24.5 hours after NotBefore - maxExpirationAfterNotBefore := token.NotBefore().Add(time.Minute * time.Duration(1470)) - if token.Expiration().After(maxExpirationAfterNotBefore) { + maxExpirationAfterNotBefore := nbf.Add(time.Minute * time.Duration(1470)) + if exp.After(maxExpirationAfterNotBefore) { return errors.New("token expires too long after nbf") } // Ensure the expiration is no more than 24.5 hours after IssuedAt - maxExpirationAfterIssuedAt := token.IssuedAt().Add(time.Minute * time.Duration(1470)) - if token.Expiration().After(maxExpirationAfterIssuedAt) { + maxExpirationAfterIssuedAt := iat.Add(time.Minute * time.Duration(1470)) + if exp.After(maxExpirationAfterIssuedAt) { return errors.New("token expires too long after iat") } // Ensure the IssuedAt is <= the NotBefore date - if token.IssuedAt().After(token.NotBefore()) { + if iat.After(nbf) { return errors.New("token nbf occurs before iat") } // Ensure the subject field is non-empty - if token.Subject() == "" { + if sub, _ := token.Subject(); sub == "" { return errors.New("sub must not be empty") } @@ -319,7 +325,7 @@ func bestPracticesCheck(token jwt.Token) error { func acceptableSignatureAlgorithm(algorithm jwa.SignatureAlgorithm) bool { switch algorithm { // The following algorithms are supported for elliptic curve keys - case jwa.ES256, jwa.ES384, jwa.ES512: + case jwa.ES256(), jwa.ES384(), jwa.ES512(): return true // The RS512/PS512 algorithms are supported for RSA keys, but less secure @@ -342,11 +348,11 @@ func acceptableSignatureAlgorithm(algorithm jwa.SignatureAlgorithm) bool { // the ssh-agent protocol. It seems the ssh ecosystem has generally moved // beyond RSA support in favor of trying to maximize security within the // scope of RSA use. Perhaps we could consider doing the same. - case jwa.RS512, jwa.PS512: + case jwa.RS512(), jwa.PS512(): return true // Edwards curve signatures are considered secure and are therefore supported - case jwa.EdDSA: + case jwa.EdDSA(): return true // Explicitly reject messages signed by the "none" algorithm. This @@ -355,7 +361,7 @@ func acceptableSignatureAlgorithm(algorithm jwa.SignatureAlgorithm) bool { // approach into a blacklist approach in the future. Not to mention this // going wrong would result in a catastrophic security hole, so it's worth // having a special case for it. - case jwa.NoSignature: + case jwa.NoSignature(): return false // Only explicitly allowed signing algorithms are acceptable diff --git a/http/tokenV2/middleware_test.go b/http/tokenV2/middleware_test.go index a2316272e3..a4805e8b84 100644 --- a/http/tokenV2/middleware_test.go +++ b/http/tokenV2/middleware_test.go @@ -40,9 +40,9 @@ import ( "github.com/labstack/echo/v4" - "github.com/lestrrat-go/jwx/v2/jwa" - "github.com/lestrrat-go/jwx/v2/jwk" - "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/lestrrat-go/jwx/v3/jwa" + "github.com/lestrrat-go/jwx/v3/jwk" + "github.com/lestrrat-go/jwx/v3/jwt" "github.com/google/uuid" @@ -70,7 +70,7 @@ func generateEd25519TestKey(t *testing.T) (jwk.Key, *jwt.Serializer, []byte) { sshAuthKey := fmt.Sprintf("%v %v %v", sshPub.Type(), b64.StdEncoding.EncodeToString(sshPub.Marshal()), validUser) // Convert the base key type to a jwk type - jwkKey, err := jwk.FromRaw(priv) + jwkKey, err := jwk.Import(priv) require.NoError(t, err) // Set the key ID for the jwk to be the public key fingerprint @@ -78,7 +78,7 @@ func generateEd25519TestKey(t *testing.T) (jwk.Key, *jwt.Serializer, []byte) { require.NoError(t, err) // Create a serializer configured to use the generated key - serializer := jwt.NewSerializer().Sign(jwt.WithKey(jwa.EdDSA, jwkKey)) + serializer := jwt.NewSerializer().Sign(jwt.WithKey(jwa.EdDSA(), jwkKey)) t.Logf("authorized_key = %v", sshAuthKey) @@ -98,7 +98,7 @@ func generateECDSATestKey(t *testing.T, curve elliptic.Curve, signingAlgorithm j sshAuthKey := fmt.Sprintf("%v %v %v", sshPub.Type(), b64.StdEncoding.EncodeToString(sshPub.Marshal()), validUser) // Convert the base key type to a jwk type - jwkKey, err := jwk.FromRaw(priv) + jwkKey, err := jwk.Import(priv) require.NoError(t, err) // Set the key ID for the jwk to be the public key fingerprint @@ -126,7 +126,7 @@ func generateRSATestKey(t *testing.T, bits int, signingAlgorithm jwa.SignatureAl sshAuthKey := fmt.Sprintf("%v %v %s", sshPub.Type(), b64.StdEncoding.EncodeToString(sshPub.Marshal()), validUser) // Convert the base key type to a jwk type - jwkKey, err := jwk.FromRaw(priv) + jwkKey, err := jwk.Import(priv) require.NoError(t, err) // Set the key ID for the jwk to be the public key fingerprint @@ -272,9 +272,9 @@ func TestAuditLogAccessGranted(t *testing.T) { assert.Equal(t, ok, recorder.Body.String()) // Ensure the audit logging is working - jwtID, _ := token.Get(jwt.JwtIDKey) - subject, _ := token.Get(jwt.SubjectKey) - issuer, _ := token.Get(jwt.IssuerKey) + jwtID, _ := token.JwtID() + subject, _ := token.Subject() + issuer, _ := token.Issuer() capturedAuditLog.AssertContains(t, "http", audit.AccessGrantedEvent, validUser, fmt.Sprintf("Access granted to user '%v' with JWT %s issued to %s by %s", validUser, jwtID, subject, issuer)) } @@ -445,7 +445,7 @@ func TestInvalidSingleAudience(t *testing.T) { // Call the handler, ensuring the appropriate error is returned err = handler(testCtx) require.Error(t, err) - assert.Contains(t, err.(*echo.HTTPError).Internal.Error(), "jwt.Validate: \"aud\" not satisfied") + assert.Contains(t, err.(*echo.HTTPError).Internal.Error(), "validation failed: \"aud\" not satisfied") } // TestValidIssProxiedSub ensures a valid JWT containing a subject mentioning a proxied username results in a 200 OK @@ -610,7 +610,7 @@ func TestValidJWTCaseInsensitiveBearer(t *testing.T) { // TestValidJWTECDSAES256 ensures a valid JWT signed by an ECDSA 256-bit key authorizes a request func TestValidJWTECDSAES256(t *testing.T) { // Generate a new test key and jwt serializer - _, serializer, authorizedKey := generateECDSATestKey(t, elliptic.P256(), jwa.ES256) + _, serializer, authorizedKey := generateECDSATestKey(t, elliptic.P256(), jwa.ES256()) // Create a new JWT token := validJWT(t) @@ -647,7 +647,7 @@ func TestValidJWTECDSAES256(t *testing.T) { // TestValidJWTECDSAES384 ensures a valid JWT signed by an ECDSA 384-bit key authorizes a request func TestValidJWTECDSAES384(t *testing.T) { // Generate a new test key and jwt serializer - _, serializer, authorizedKey := generateECDSATestKey(t, elliptic.P384(), jwa.ES384) + _, serializer, authorizedKey := generateECDSATestKey(t, elliptic.P384(), jwa.ES384()) // Create a new JWT token := validJWT(t) @@ -684,7 +684,7 @@ func TestValidJWTECDSAES384(t *testing.T) { // TestValidJWTECDSAES384 ensures a valid JWT signed by an ECDSA 384-bit key authorizes a request func TestValidJWTECDSAES512(t *testing.T) { // Generate a new test key and jwt serializer - _, serializer, authorizedKey := generateECDSATestKey(t, elliptic.P521(), jwa.ES512) + _, serializer, authorizedKey := generateECDSATestKey(t, elliptic.P521(), jwa.ES512()) // Create a new JWT token := validJWT(t) @@ -754,7 +754,7 @@ func TestWrongAudienceJWT(t *testing.T) { // Call the handler, ensuring the appropriate error is returned err = handler(testCtx) require.Error(t, err) - assert.Contains(t, err.(*echo.HTTPError).Internal.Error(), "jwt.Validate: \"aud\" not satisfied") + assert.Contains(t, err.(*echo.HTTPError).Internal.Error(), "validation failed: \"aud\" not satisfied") } // TestWrongKeyID ensures a JWT with the wrong kid from an authorized key causes 401 Unauthorized @@ -811,7 +811,7 @@ func TestCorrectKeyIDWithIncorrectSignature(t *testing.T) { token := validJWT(t) // Sign and serialize the JWT with the untrusted key, setting the key id to a trusted key - trustedKeyID, found := trustedKey.Get(jwk.KeyIDKey) + trustedKeyID, found := trustedKey.KeyID() require.True(t, found) require.NoError(t, untrustedKey.Set(jwk.KeyIDKey, trustedKeyID)) serialized, err := untrustedSerializer.Serialize(token) @@ -881,7 +881,7 @@ func TestExpiredJWT(t *testing.T) { // Call the handler, ensuring the appropriate error is returned err = handler(testCtx) assert.Error(t, err) - assert.Contains(t, err.(*echo.HTTPError).Internal.Error(), "jwt.Validate: \"exp\" not satisfied") + assert.Contains(t, err.(*echo.HTTPError).Internal.Error(), "validation failed: \"exp\" not satisfied") } // TestFutureIATJWT ensures a not yet valid (iat = future) JWT from an authorized key causes 401 Unauthorized @@ -924,7 +924,7 @@ func TestFutureIATJWT(t *testing.T) { // Call the handler, ensuring the appropriate error is returned err = handler(testCtx) require.Error(t, err) - assert.Contains(t, err.(*echo.HTTPError).Internal.Error(), "jwt.Validate: \"iat\" not satisfied") + assert.Contains(t, err.(*echo.HTTPError).Internal.Error(), "validation failed: \"iat\" not satisfied") } // TestFutureNBFJWT ensures a not yet valid (nbf = future) JWT from an authorized key causes 401 Unauthorized @@ -967,7 +967,7 @@ func TestFutureNBFJWT(t *testing.T) { // Call the handler, ensuring the appropriate error is returned err = handler(testCtx) require.Error(t, err) - assert.Contains(t, err.(*echo.HTTPError).Internal.Error(), "jwt.Validate: \"nbf\" not satisfied") + assert.Contains(t, err.(*echo.HTTPError).Internal.Error(), "validation failed: \"nbf\" not satisfied") } // TestUnauthorizedKey ensures a valid JWT signed by an unauthorized key is rejected with 401 Unauthorized @@ -1163,7 +1163,7 @@ func TestNonBearerToken(t *testing.T) { // The RS512 signing algorithm is a suitable alternative, or better yet do not use RSA keys at all. func TestInsecureRS256JWT(t *testing.T) { // Generate a new test key and jwt serializer - _, serializer, authorizedKey := generateRSATestKey(t, 4096, jwa.RS256) + _, serializer, authorizedKey := generateRSATestKey(t, 4096, jwa.RS256()) // Create a new JWT token := validJWT(t) @@ -1202,7 +1202,7 @@ func TestInsecureRS256JWT(t *testing.T) { // The RS512 signing algorithm is a suitable alternative, or better yet do not use RSA keys at all. func TestInsecureRS384JWT(t *testing.T) { // Generate a new test key and jwt serializer - _, serializer, authorizedKey := generateRSATestKey(t, 4096, jwa.RS384) + _, serializer, authorizedKey := generateRSATestKey(t, 4096, jwa.RS384()) // Create a new JWT token := validJWT(t) @@ -1237,55 +1237,26 @@ func TestInsecureRS384JWT(t *testing.T) { assert.Contains(t, err.(*echo.HTTPError).Internal.Error(), "insecure credential: signing algorithm RS384 is not permitted") } -// TestInsecure1024BitRS512JWT ensures a JWT signed securely with an RS512 signing algorithm but using an insecure 1024-bit RSA key is rejected with a 401 Unauthorized response +// TestInsecure1024BitRS512JWT ensures an insecure 1024-bit RSA key is rejected outright. +// jwx v3 refuses to import RSA keys smaller than 2048 bits, so such a key can never be used to +// sign a JWT nor be loaded into the authorized keys list. This is a stronger guarantee than the +// previous behaviour, where the key was rejected only because it never made it into the in-memory +// authorized keys list (see also authorized_keys_test.go, which rejects the key at that level). func TestInsecure1024BitRS512JWT(t *testing.T) { - // Generate a new test key and jwt serializer - _, serializer, authorizedKey := generateRSATestKey(t, 1024, jwa.RS512) - - // Create a new JWT - token := validJWT(t) - - // Sign and serialize the JWT - serialized, err := serializer.Serialize(token) - require.NoError(t, err) - t.Logf("jwt=%v", string(serialized)) - - // Create the middleware - middleware, err := New(nil, validHostname, []byte(authorizedKey)) + // Generate a new insecure 1024-bit RSA key + priv, err := rsa.GenerateKey(rand.Reader, 1024) require.NoError(t, err) - // Setup the handler such that if the middleware authorizes the request a 200 OK response is set - handler := middleware.Handler(statusOKHandler) - - // Create a test GET request - request, err := http.NewRequest("GET", "/", nil) - require.NoError(t, err) - - // Set the authorization header in the test request - header := fmt.Sprintf("Bearer %v", string(serialized)) - request.Header.Set("Authorization", header) - - // Setup a test context which wraps the test request and records the response - recorder := httptest.NewRecorder() - testCtx := echo.New().NewContext(request, recorder) - - // Call the handler, ensuring no error is returned - err = handler(testCtx) + // Ensure the insecure key cannot be imported into a jwk, which prevents it from ever being used + _, err = jwk.Import(priv) require.Error(t, err) - - // Note that this error may seem a bit strange, but the key is not authorized as 1024-bit RSA keys should never make their way - // into the authorized keys list in memory. This technically doesn't provide the best guarantee as to the reason for failure - // in this unit test but it is the best that can be done at the time and it is likely not desirable for the 1024-bit RSA key - // to be loaded into the list in memory anyways. We take a small chance here but it's an extreme corner case and as long as - // we are careful when modifying this test, this should be sufficient along with the tests in authorized_keys_test.go which - // ensure the keys are rejected at that level. - assert.Contains(t, err.(*echo.HTTPError).Internal.Error(), "credential not signed by an authorized key") + assert.Contains(t, err.Error(), "rsa modulus too small") } // TestSecureRS512JWT ensures a JWT signed securely with an RS512 signing algorithm and a 4096-bit RSA key is accepted with a 200 OK response func TestSecureRS512JWT(t *testing.T) { // Generate a new test key and jwt serializer - _, serializer, authorizedKey := generateRSATestKey(t, 4096, jwa.RS512) + _, serializer, authorizedKey := generateRSATestKey(t, 4096, jwa.RS512()) // Create a new JWT token := validJWT(t) @@ -1329,7 +1300,7 @@ func TestSecureRS512JWT(t *testing.T) { // at all. func TestPS256JWT(t *testing.T) { // Generate a new test key and jwt serializer - _, serializer, authorizedKey := generateRSATestKey(t, 4096, jwa.PS256) + _, serializer, authorizedKey := generateRSATestKey(t, 4096, jwa.PS256()) // Create a new JWT token := validJWT(t) @@ -1369,7 +1340,7 @@ func TestPS256JWT(t *testing.T) { // at all. func TestPS384JWT(t *testing.T) { // Generate a new test key and jwt serializer - _, serializer, authorizedKey := generateRSATestKey(t, 4096, jwa.PS384) + _, serializer, authorizedKey := generateRSATestKey(t, 4096, jwa.PS384()) // Create a new JWT token := validJWT(t) @@ -1407,7 +1378,7 @@ func TestPS384JWT(t *testing.T) { // TestSecurePS512JWT ensures a JWT signed securely with an RS512 signing algorithm and a 4096-bit RSA key is accepted with a 200 OK response func TestSecurePS512JWT(t *testing.T) { // Generate a new test key and jwt serializer - _, serializer, authorizedKey := generateRSATestKey(t, 4096, jwa.PS512) + _, serializer, authorizedKey := generateRSATestKey(t, 4096, jwa.PS512()) // Create a new JWT token := validJWT(t) @@ -1598,7 +1569,7 @@ func TestMissingAud(t *testing.T) { // Call the handler, ensuring the appropriate error is returned err = handler(testCtx) require.Error(t, err) - assert.Contains(t, err.(*echo.HTTPError).Internal.Error(), "jwt.Validate: claim \"aud\" not found") + assert.Contains(t, err.(*echo.HTTPError).Internal.Error(), "claim \"aud\" does not exist") } // TestMissingIss ensures a JWT with a missing issuer is rejected with 401 Unauthorized diff --git a/http/user/session.go b/http/user/session.go index 41dce47aac..1e81933e7a 100644 --- a/http/user/session.go +++ b/http/user/session.go @@ -26,7 +26,7 @@ import ( "fmt" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" - "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v3/jwk" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/go-did/vc" "github.com/nuts-foundation/nuts-node/auth/log" diff --git a/http/user/session_test.go b/http/user/session_test.go index 871448cb40..3695402bd8 100644 --- a/http/user/session_test.go +++ b/http/user/session_test.go @@ -24,7 +24,7 @@ import ( "crypto/rand" "encoding/json" "github.com/labstack/echo/v4" - "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v3/jwk" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/go-did/vc" "github.com/nuts-foundation/nuts-node/storage" @@ -264,7 +264,7 @@ func TestMiddleware_createUserSessionCookie(t *testing.T) { func TestUserWallet_Key(t *testing.T) { t.Run("ok", func(t *testing.T) { pk, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - keyAsJWK, err := jwk.FromRaw(pk) + keyAsJWK, err := jwk.Import(pk) require.NoError(t, err) jwkAsJSON, _ := json.Marshal(keyAsJWK) wallet := Wallet{ diff --git a/network/dag/parser.go b/network/dag/parser.go index e1be15758f..1cf3464bba 100644 --- a/network/dag/parser.go +++ b/network/dag/parser.go @@ -23,9 +23,9 @@ import ( "fmt" "time" - "github.com/lestrrat-go/jwx/v2/jwa" - "github.com/lestrrat-go/jwx/v2/jwk" - "github.com/lestrrat-go/jwx/v2/jws" + "github.com/lestrrat-go/jwx/v3/jwa" + "github.com/lestrrat-go/jwx/v3/jwk" + "github.com/lestrrat-go/jwx/v3/jws" "github.com/nuts-foundation/nuts-node/crypto/hash" ) @@ -77,8 +77,9 @@ type transactionParseStep func(transaction *transaction, headers jws.Headers, me // parseSigningAlgorithm validates whether the signing algorithm is allowed func parseSigningAlgorithm(_ *transaction, headers jws.Headers, _ *jws.Message) error { - if !isAlgoAllowed(headers.Algorithm()) { - return transactionValidationError("signing algorithm not allowed: %s", headers.Algorithm()) + alg, _ := headers.Algorithm() + if !isAlgoAllowed(alg) { + return transactionValidationError("signing algorithm not allowed: %s", alg) } return nil } @@ -95,7 +96,7 @@ func parsePayload(transaction *transaction, _ jws.Headers, message *jws.Message) // parseContentType parses, validates and sets the transaction payload content type. func parseContentType(transaction *transaction, headers jws.Headers, _ *jws.Message) error { - contentType := headers.ContentType() + contentType, _ := headers.ContentType() if !ValidatePayloadType(contentType) { return transactionValidationError("%w", errInvalidPayloadType) } @@ -105,25 +106,27 @@ func parseContentType(transaction *transaction, headers jws.Headers, _ *jws.Mess // parseSignatureParams parses, validates and sets the transaction signing key (`jwk`) or key ID (`kid`). func parseSignatureParams(transaction *transaction, headers jws.Headers, _ *jws.Message) error { - if key, ok := headers.Get(jws.JWKKey); ok { - jwkKey := key.(jwk.Key) + var jwkKey jwk.Key + if err := headers.Get(jws.JWKKey, &jwkKey); err == nil { transaction.signingKey = jwkKey } // Get the keyID from the header (not to be confused with the keyID from the embedded key) - if kid, ok := headers.Get(jws.KeyIDKey); ok { - transaction.signingKeyID = kid.(string) + var kid string + if err := headers.Get(jws.KeyIDKey, &kid); err == nil { + transaction.signingKeyID = kid } // Check RFC004 3.1 kid and jwk constraints if (transaction.signingKey != nil && transaction.signingKeyID != "") || (transaction.signingKey == nil && transaction.signingKeyID == "") { return transactionValidationError("either `kid` or `jwk` header must be present (but not both)") } - transaction.signingAlgorithm = headers.Algorithm() + transaction.signingAlgorithm, _ = headers.Algorithm() return nil } // parseSigningTime parses, validates and sets the transaction signing time. func parseSigningTime(transaction *transaction, headers jws.Headers, _ *jws.Message) error { - if timeAsInterf, ok := headers.Get(signingTimeHeader); !ok { + var timeAsInterf interface{} + if err := headers.Get(signingTimeHeader, &timeAsInterf); err != nil { return transactionValidationError(missingHeaderErrFmt, signingTimeHeader) } else if timeAsFloat64, ok := timeAsInterf.(float64); !ok { return transactionValidationError(invalidHeaderErrFmt, signingTimeHeader) @@ -136,7 +139,8 @@ func parseSigningTime(transaction *transaction, headers jws.Headers, _ *jws.Mess // parseVersion parses, validates and sets the transaction format version. func parseVersion(transaction *transaction, headers jws.Headers, _ *jws.Message) error { var version Version - if versionAsInterf, ok := headers.Get(versionHeader); !ok { + var versionAsInterf interface{} + if err := headers.Get(versionHeader, &versionAsInterf); err != nil { return transactionValidationError(missingHeaderErrFmt, versionHeader) } else if versionAsFloat64, ok := versionAsInterf.(float64); !ok { return transactionValidationError(invalidHeaderErrFmt, versionHeader) @@ -159,7 +163,8 @@ func versionAllowed(version Version) bool { // parsePrevious parses, validates and sets the transaction prevs fields. func parsePrevious(transaction *transaction, headers jws.Headers, _ *jws.Message) error { - if prevsAsInterf, ok := headers.Get(previousHeader); !ok { + var prevsAsInterf interface{} + if err := headers.Get(previousHeader, &prevsAsInterf); err != nil { return transactionValidationError(missingHeaderErrFmt, previousHeader) } else if prevsAsSlice, ok := prevsAsInterf.([]interface{}); !ok { return transactionValidationError(invalidHeaderErrFmt, previousHeader) @@ -178,8 +183,8 @@ func parsePrevious(transaction *transaction, headers jws.Headers, _ *jws.Message } func parsePAL(transaction *transaction, headers jws.Headers, _ *jws.Message) error { - rawPal, ok := headers.Get(palHeader) - if !ok { + var rawPal interface{} + if err := headers.Get(palHeader, &rawPal); err != nil { return nil } palEncoded, ok := rawPal.([]interface{}) @@ -199,7 +204,8 @@ func parsePAL(transaction *transaction, headers jws.Headers, _ *jws.Message) err } func parseLamportClock(transaction *transaction, headers jws.Headers, _ *jws.Message) error { - if lcAsInterf, ok := headers.Get(lamportClockHeader); !ok { + var lcAsInterf interface{} + if err := headers.Get(lamportClockHeader, &lcAsInterf); err != nil { // won't happen since it's a critical header, but we need to check the cast anyway return transactionValidationError(missingHeaderErrFmt, lamportClockHeader) } else if lcAsFloat64, ok := lcAsInterf.(float64); !ok { diff --git a/network/dag/parser_test.go b/network/dag/parser_test.go index 3d84ed5377..bb74a18244 100644 --- a/network/dag/parser_test.go +++ b/network/dag/parser_test.go @@ -26,9 +26,9 @@ import ( "testing" "time" - "github.com/lestrrat-go/jwx/v2/jwa" - "github.com/lestrrat-go/jwx/v2/jwk" - "github.com/lestrrat-go/jwx/v2/jws" + "github.com/lestrrat-go/jwx/v3/jwa" + "github.com/lestrrat-go/jwx/v3/jwk" + "github.com/lestrrat-go/jwx/v3/jws" "github.com/nuts-foundation/nuts-node/crypto/hash" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" @@ -41,13 +41,13 @@ func TestParseTransaction(t *testing.T) { t.Run("v1", func(t *testing.T) { headers := makeJWSHeaders(key, "123", true) _ = headers.Set("pal", []string{base64.StdEncoding.EncodeToString([]byte{5, 6, 7})}) - signature, _ := jws.Sign(payloadAsBytes, jws.WithKey(headers.Algorithm(), key, jws.WithProtectedHeaders(headers))) + signature, _ := jws.Sign(payloadAsBytes, jws.WithKey(algOf(headers), key, jws.WithProtectedHeaders(headers))) transaction, err := ParseTransaction(signature) require.NoError(t, err) var actualKey ecdsa.PublicKey - err = transaction.SigningKey().Raw(&actualKey) + err = jwk.Export(transaction.SigningKey(), &actualKey) require.NoError(t, err) assert.NotNil(t, transaction) @@ -58,7 +58,9 @@ func TestParseTransaction(t *testing.T) { assert.Equal(t, 1, int(transaction.Version())) assert.Equal(t, "foo/bar", transaction.PayloadType()) assert.Equal(t, time.UTC, transaction.SigningTime().Location()) - assert.Equal(t, headers.PrivateParams()[previousHeader].([]string)[0], transaction.Previous()[0].String()) + var previousHeaderValue []string + _ = headers.Get(previousHeader, &previousHeaderValue) + assert.Equal(t, previousHeaderValue[0], transaction.Previous()[0].String()) assert.Equal(t, transaction.PAL(), [][]byte{{5, 6, 7}}) assert.NotNil(t, transaction.Data()) assert.False(t, transaction.Ref().Empty()) @@ -68,13 +70,13 @@ func TestParseTransaction(t *testing.T) { _ = headers.Set("pal", []string{base64.StdEncoding.EncodeToString([]byte{5, 6, 7})}) _ = headers.Set(versionHeader, 2) _ = headers.Set(jws.CriticalKey, []string{signingTimeHeader, versionHeader, previousHeader, lamportClockHeader}) - signature, _ := jws.Sign(payloadAsBytes, jws.WithKey(headers.Algorithm(), key, jws.WithProtectedHeaders(headers))) + signature, _ := jws.Sign(payloadAsBytes, jws.WithKey(algOf(headers), key, jws.WithProtectedHeaders(headers))) transaction, err := ParseTransaction(signature) require.NoError(t, err) var actualKey ecdsa.PublicKey - err = transaction.SigningKey().Raw(&actualKey) + err = jwk.Export(transaction.SigningKey(), &actualKey) require.NoError(t, err) assert.NotNil(t, transaction) @@ -85,7 +87,9 @@ func TestParseTransaction(t *testing.T) { assert.Equal(t, 2, int(transaction.Version())) assert.Equal(t, "foo/bar", transaction.PayloadType()) assert.Equal(t, time.UTC, transaction.SigningTime().Location()) - assert.Equal(t, headers.PrivateParams()[previousHeader].([]string)[0], transaction.Previous()[0].String()) + var previousHeaderValue []string + _ = headers.Get(previousHeader, &previousHeaderValue) + assert.Equal(t, previousHeaderValue[0], transaction.Previous()[0].String()) assert.Equal(t, transaction.PAL(), [][]byte{{5, 6, 7}}) assert.NotNil(t, transaction.Data()) assert.False(t, transaction.Ref().Empty()) @@ -93,18 +97,18 @@ func TestParseTransaction(t *testing.T) { t.Run("error - input not a JWS (compact serialization format)", func(t *testing.T) { tx, err := ParseTransaction([]byte("not a JWS")) assert.Nil(t, tx) - assert.EqualError(t, err, "unable to parse transaction: invalid compact serialization format: invalid number of segments") + assert.EqualError(t, err, "unable to parse transaction: jws.Parse: failed to parse compact format: jws.Parse: invalid compact serialization format: jwsbb: invalid number of segments") }) t.Run("error - input not a JWS (JSON serialization format)", func(t *testing.T) { tx, err := ParseTransaction([]byte("{}")) assert.Nil(t, tx) - assert.EqualError(t, err, "unable to parse transaction: failed to unmarshal jws message: required field \"signatures\" not present") + assert.EqualError(t, err, "unable to parse transaction: jws.Parse: failed to parse JSON format: failed to unmarshal jws message: required field \"signatures\" not present") }) t.Run("error - pal header has invalid type", func(t *testing.T) { headers := makeJWSHeaders(key, "123", false) _ = headers.Set("pal", 100) - signature, _ := jws.Sign(payloadAsBytes, jws.WithKey(headers.Algorithm(), key, jws.WithProtectedHeaders(headers))) + signature, _ := jws.Sign(payloadAsBytes, jws.WithKey(algOf(headers), key, jws.WithProtectedHeaders(headers))) transaction, err := ParseTransaction(signature) @@ -113,8 +117,8 @@ func TestParseTransaction(t *testing.T) { }) t.Run("error - sigt header is missing", func(t *testing.T) { headers := makeJWSHeaders(key, "123", false) - delete(headers.PrivateParams(), signingTimeHeader) - signature, _ := jws.Sign(payloadAsBytes, jws.WithKey(headers.Algorithm(), key, jws.WithProtectedHeaders(headers))) + _ = headers.Remove(signingTimeHeader) + signature, _ := jws.Sign(payloadAsBytes, jws.WithKey(algOf(headers), key, jws.WithProtectedHeaders(headers))) transaction, err := ParseTransaction(signature) @@ -124,7 +128,7 @@ func TestParseTransaction(t *testing.T) { t.Run("error - invalid sigt header", func(t *testing.T) { headers := makeJWSHeaders(key, "123", false) headers.Set(signingTimeHeader, "not a date") - signature, _ := jws.Sign(payloadAsBytes, jws.WithKey(headers.Algorithm(), key, jws.WithProtectedHeaders(headers))) + signature, _ := jws.Sign(payloadAsBytes, jws.WithKey(algOf(headers), key, jws.WithProtectedHeaders(headers))) transaction, err := ParseTransaction(signature) @@ -133,8 +137,8 @@ func TestParseTransaction(t *testing.T) { }) t.Run("error - vers header is missing", func(t *testing.T) { headers := makeJWSHeaders(key, "123", false) - delete(headers.PrivateParams(), versionHeader) - signature, _ := jws.Sign(payloadAsBytes, jws.WithKey(headers.Algorithm(), key, jws.WithProtectedHeaders(headers))) + _ = headers.Remove(versionHeader) + signature, _ := jws.Sign(payloadAsBytes, jws.WithKey(algOf(headers), key, jws.WithProtectedHeaders(headers))) transaction, err := ParseTransaction(signature) @@ -144,7 +148,7 @@ func TestParseTransaction(t *testing.T) { t.Run("error - both jwk and kid set", func(t *testing.T) { headers := makeJWSHeaders(key, "1234", true) headers.Set(jwk.KeyIDKey, "123") - signature, _ := jws.Sign(payloadAsBytes, jws.WithKey(headers.Algorithm(), key, jws.WithProtectedHeaders(headers))) + signature, _ := jws.Sign(payloadAsBytes, jws.WithKey(algOf(headers), key, jws.WithProtectedHeaders(headers))) transaction, err := ParseTransaction(signature) @@ -153,7 +157,7 @@ func TestParseTransaction(t *testing.T) { }) t.Run("error - jwk/kid both not set", func(t *testing.T) { headers := makeJWSHeaders(nil, "", false) - signature, _ := jws.Sign(payloadAsBytes, jws.WithKey(headers.Algorithm(), key, jws.WithProtectedHeaders(headers))) + signature, _ := jws.Sign(payloadAsBytes, jws.WithKey(algOf(headers), key, jws.WithProtectedHeaders(headers))) transaction, err := ParseTransaction(signature) @@ -162,8 +166,8 @@ func TestParseTransaction(t *testing.T) { }) t.Run("error - prevs header is missing", func(t *testing.T) { headers := makeJWSHeaders(key, "123", true) - delete(headers.PrivateParams(), previousHeader) - signature, _ := jws.Sign(payloadAsBytes, jws.WithKey(headers.Algorithm(), key, jws.WithProtectedHeaders(headers))) + _ = headers.Remove(previousHeader) + signature, _ := jws.Sign(payloadAsBytes, jws.WithKey(algOf(headers), key, jws.WithProtectedHeaders(headers))) transaction, err := ParseTransaction(signature) @@ -173,7 +177,7 @@ func TestParseTransaction(t *testing.T) { t.Run("error - invalid prevs (not an array)", func(t *testing.T) { headers := makeJWSHeaders(key, "123", true) headers.Set(previousHeader, 2) - signature, _ := jws.Sign(payloadAsBytes, jws.WithKey(headers.Algorithm(), key, jws.WithProtectedHeaders(headers))) + signature, _ := jws.Sign(payloadAsBytes, jws.WithKey(algOf(headers), key, jws.WithProtectedHeaders(headers))) transaction, err := ParseTransaction(signature) @@ -183,7 +187,7 @@ func TestParseTransaction(t *testing.T) { t.Run("error - invalid prevs (invalid entry)", func(t *testing.T) { headers := makeJWSHeaders(key, "123", true) headers.Set(previousHeader, []string{"not a hash"}) - signature, _ := jws.Sign(payloadAsBytes, jws.WithKey(headers.Algorithm(), key, jws.WithProtectedHeaders(headers))) + signature, _ := jws.Sign(payloadAsBytes, jws.WithKey(algOf(headers), key, jws.WithProtectedHeaders(headers))) transaction, err := ParseTransaction(signature) @@ -193,7 +197,7 @@ func TestParseTransaction(t *testing.T) { t.Run("error - invalid prevs (invalid entry, not a string)", func(t *testing.T) { headers := makeJWSHeaders(key, "123", true) headers.Set(previousHeader, []int{5}) - signature, _ := jws.Sign(payloadAsBytes, jws.WithKey(headers.Algorithm(), key, jws.WithProtectedHeaders(headers))) + signature, _ := jws.Sign(payloadAsBytes, jws.WithKey(algOf(headers), key, jws.WithProtectedHeaders(headers))) transaction, err := ParseTransaction(signature) @@ -203,7 +207,7 @@ func TestParseTransaction(t *testing.T) { t.Run("error - cty header is invalid", func(t *testing.T) { headers := makeJWSHeaders(key, "", false) headers.Set(jws.ContentTypeKey, "") - signature, _ := jws.Sign(payloadAsBytes, jws.WithKey(headers.Algorithm(), key, jws.WithProtectedHeaders(headers))) + signature, _ := jws.Sign(payloadAsBytes, jws.WithKey(algOf(headers), key, jws.WithProtectedHeaders(headers))) transaction, err := ParseTransaction(signature) @@ -213,7 +217,7 @@ func TestParseTransaction(t *testing.T) { t.Run("error - invalid version", func(t *testing.T) { headers := makeJWSHeaders(key, "123", true) headers.Set(versionHeader, "foobar") - signature, _ := jws.Sign(payloadAsBytes, jws.WithKey(headers.Algorithm(), key, jws.WithProtectedHeaders(headers))) + signature, _ := jws.Sign(payloadAsBytes, jws.WithKey(algOf(headers), key, jws.WithProtectedHeaders(headers))) transaction, err := ParseTransaction(signature) @@ -223,7 +227,7 @@ func TestParseTransaction(t *testing.T) { t.Run("error - unsupported version", func(t *testing.T) { headers := makeJWSHeaders(key, "123", true) headers.Set(versionHeader, 3) - signature, _ := jws.Sign(payloadAsBytes, jws.WithKey(headers.Algorithm(), key, jws.WithProtectedHeaders(headers))) + signature, _ := jws.Sign(payloadAsBytes, jws.WithKey(algOf(headers), key, jws.WithProtectedHeaders(headers))) transaction, err := ParseTransaction(signature) @@ -233,8 +237,8 @@ func TestParseTransaction(t *testing.T) { t.Run("error - invalid algorithm", func(t *testing.T) { key := generateRSAKey() headers := makeJWSHeaders(key, "", false) - headers.Set(jws.AlgorithmKey, jwa.RS256) - signature, _ := jws.Sign(payloadAsBytes, jws.WithKey(headers.Algorithm(), key, jws.WithProtectedHeaders(headers))) + headers.Set(jws.AlgorithmKey, jwa.RS256()) + signature, _ := jws.Sign(payloadAsBytes, jws.WithKey(algOf(headers), key, jws.WithProtectedHeaders(headers))) transaction, err := ParseTransaction(signature) @@ -244,7 +248,7 @@ func TestParseTransaction(t *testing.T) { t.Run("error - invalid lamport clock", func(t *testing.T) { headers := makeJWSHeaders(key, "123", true) headers.Set(lamportClockHeader, "a") - signature, _ := jws.Sign(payloadAsBytes, jws.WithKey(headers.Algorithm(), key, jws.WithProtectedHeaders(headers))) + signature, _ := jws.Sign(payloadAsBytes, jws.WithKey(algOf(headers), key, jws.WithProtectedHeaders(headers))) transaction, err := ParseTransaction(signature) @@ -253,7 +257,7 @@ func TestParseTransaction(t *testing.T) { }) t.Run("error - invalid payload", func(t *testing.T) { headers := makeJWSHeaders(key, "123", true) - signature, _ := jws.Sign([]byte("not a valid hash"), jws.WithKey(headers.Algorithm(), key, jws.WithProtectedHeaders(headers))) + signature, _ := jws.Sign([]byte("not a valid hash"), jws.WithKey(algOf(headers), key, jws.WithProtectedHeaders(headers))) transaction, err := ParseTransaction(signature) @@ -262,10 +266,15 @@ func TestParseTransaction(t *testing.T) { }) } +func algOf(headers jws.Headers) jwa.SignatureAlgorithm { + alg, _ := headers.Algorithm() + return alg +} + func makeJWSHeaders(key crypto.Signer, kid string, embedKey bool) jws.Headers { prev, _ := hash.ParseHex("bedcd5bfb50af622be56c4aec7ac5da64745686b362afc7e615ea89b0705b8f8") headerMap := map[string]interface{}{ - jws.AlgorithmKey: jwa.ES256, + jws.AlgorithmKey: jwa.ES256(), jws.ContentTypeKey: "foo/bar", jws.CriticalKey: []string{signingTimeHeader, versionHeader, previousHeader, lamportClockHeader}, lamportClockHeader: 0, @@ -274,7 +283,7 @@ func makeJWSHeaders(key crypto.Signer, kid string, embedKey bool) jws.Headers { previousHeader: []string{prev.String()}, } if embedKey { - keyAsJWS, _ := jwk.FromRaw(key.Public()) + keyAsJWS, _ := jwk.Import(key.Public()) keyAsJWS.Set(jwk.KeyIDKey, kid) headerMap[jws.JWKKey] = keyAsJWS } else { diff --git a/network/dag/signing.go b/network/dag/signing.go index 717315bd0b..a623b0469e 100644 --- a/network/dag/signing.go +++ b/network/dag/signing.go @@ -25,8 +25,8 @@ import ( "fmt" "time" - "github.com/lestrrat-go/jwx/v2/jwk" - "github.com/lestrrat-go/jwx/v2/jws" + "github.com/lestrrat-go/jwx/v3/jwk" + "github.com/lestrrat-go/jwx/v3/jws" nutsCrypto "github.com/nuts-foundation/nuts-node/crypto" ) @@ -67,7 +67,7 @@ func (d transactionSigner) Sign(ctx context.Context, input UnsignedTransaction, var key jwk.Key var err error if d.key != nil { - key, err = jwk.FromRaw(d.key) + key, err = jwk.Import(d.key) if err != nil { return nil, fmt.Errorf(errSigningTransactionFmt, err) } diff --git a/network/dag/signing_test.go b/network/dag/signing_test.go index bc886031bf..5883391a0f 100644 --- a/network/dag/signing_test.go +++ b/network/dag/signing_test.go @@ -19,7 +19,7 @@ package dag import ( - "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v3/jwk" "github.com/nuts-foundation/nuts-node/audit" "github.com/stretchr/testify/require" "testing" diff --git a/network/dag/test.go b/network/dag/test.go index 478c112e48..7e5eab7277 100644 --- a/network/dag/test.go +++ b/network/dag/test.go @@ -23,7 +23,7 @@ import ( "crypto" "encoding/binary" "fmt" - "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v3/jwk" "github.com/nuts-foundation/go-stoabs" "github.com/nuts-foundation/nuts-node/audit" "path" @@ -70,7 +70,7 @@ func CreateSignedTestTransaction(payloadNum uint32, signingTime time.Time, pal [ func jwkToCryptoPublicKey(jwkKey jwk.Key) crypto.PublicKey { jwkPublicKey, _ := jwkKey.PublicKey() var rawKey interface{} - if err := jwkPublicKey.Raw(&rawKey); err != nil { + if err := jwk.Export(jwkPublicKey, &rawKey); err != nil { panic(err) } diff --git a/network/dag/transaction.go b/network/dag/transaction.go index 39cdeaf0d1..55debee866 100644 --- a/network/dag/transaction.go +++ b/network/dag/transaction.go @@ -25,8 +25,8 @@ import ( "strings" "time" - "github.com/lestrrat-go/jwx/v2/jwa" - "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v3/jwa" + "github.com/lestrrat-go/jwx/v3/jwk" "github.com/nuts-foundation/nuts-node/crypto/hash" ) @@ -45,7 +45,7 @@ const ( lamportClockHeader = "lc" ) -var allowedAlgos = []jwa.SignatureAlgorithm{jwa.ES256, jwa.ES384, jwa.ES512, jwa.PS256, jwa.PS384, jwa.PS512} +var allowedAlgos = []jwa.SignatureAlgorithm{jwa.ES256(), jwa.ES384(), jwa.ES512(), jwa.PS256(), jwa.PS384(), jwa.PS512()} var errInvalidPayloadType = errors.New("payload type must be formatted as MIME type") var errInvalidPrevs = errors.New("prevs contains an empty hash") diff --git a/network/dag/verifier.go b/network/dag/verifier.go index 1de2887cc5..3950610e4d 100644 --- a/network/dag/verifier.go +++ b/network/dag/verifier.go @@ -25,8 +25,9 @@ import ( "github.com/nuts-foundation/go-stoabs" "github.com/nuts-foundation/nuts-node/vdr/resolver" - "github.com/lestrrat-go/jwx/v2/jwa" - "github.com/lestrrat-go/jwx/v2/jws" + "github.com/lestrrat-go/jwx/v3/jwa" + "github.com/lestrrat-go/jwx/v3/jwk" + "github.com/lestrrat-go/jwx/v3/jws" ) // ErrPreviousTransactionMissing indicates one or more of the previous transactions (which the transaction refers to) @@ -45,7 +46,7 @@ func NewTransactionSignatureVerifier(resolver resolver.NutsKeyResolver) Verifier return func(_ stoabs.ReadTx, transaction Transaction) error { var signingKey crypto2.PublicKey if transaction.SigningKey() != nil { - if err := transaction.SigningKey().Raw(&signingKey); err != nil { + if err := jwk.Export(transaction.SigningKey(), &signingKey); err != nil { return err } } else { @@ -57,7 +58,11 @@ func NewTransactionSignatureVerifier(resolver resolver.NutsKeyResolver) Verifier } // TODO: jws.Verify parses the JWS again, which we already did when parsing the transaction. If we want to optimize // this we need to implement a custom verifier. - _, err := jws.Verify(transaction.Data(), jws.WithKey(jwa.SignatureAlgorithm(transaction.SigningAlgorithm()), signingKey)) + alg, ok := jwa.LookupSignatureAlgorithm(transaction.SigningAlgorithm()) + if !ok { + return fmt.Errorf("unsupported signing algorithm: %s", transaction.SigningAlgorithm()) + } + _, err := jws.Verify(transaction.Data(), jws.WithKey(alg, signingKey)) return err } } diff --git a/network/dag/verifier_test.go b/network/dag/verifier_test.go index feda14b7ae..af6fbddd13 100644 --- a/network/dag/verifier_test.go +++ b/network/dag/verifier_test.go @@ -31,7 +31,7 @@ import ( "testing" "time" - "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v3/jwk" "github.com/nuts-foundation/go-stoabs" nutsCrypto "github.com/nuts-foundation/nuts-node/crypto" "github.com/nuts-foundation/nuts-node/crypto/hash" @@ -111,14 +111,14 @@ func TestTransactionSignatureVerifier(t *testing.T) { transaction, _, _ := CreateTestTransaction(1) expected, _ := ParseTransaction(transaction.Data()) err := NewTransactionSignatureVerifier(&staticKeyResolver{Key: attackerKey.Public()})(nil, expected) - assert.EqualError(t, err, "could not verify message using any of the signatures or keys") + assert.EqualError(t, err, "jws.Verify: could not verify message using any of the signatures or keys: jws.Verify: failed to verify signature #1 with key *ecdsa.PublicKey: invalid ECDSA signature\njws.Verify: signature #1: tried 1 key(s) but none verified successfully") }) t.Run("key type is incorrect", func(t *testing.T) { d, _, _ := CreateTestTransaction(1) tx := d.(*transaction) - tx.signingKey, _ = jwk.FromRaw([]byte("01234567890123456789012345678901234567890123456789ABCDEF")) + tx.signingKey, _ = jwk.Import([]byte("01234567890123456789012345678901234567890123456789ABCDEF")) err := NewTransactionSignatureVerifier(nil)(nil, tx) - assert.EqualError(t, err, "could not verify message using any of the signatures or keys") + assert.EqualError(t, err, "jwk.Export: no suitable exporter found for key type '*jwk.symmetricKey'") }) t.Run("unable to resolve key by hash", func(t *testing.T) { d := CreateSignedTestTransaction(1, time.Now(), nil, "foo/bar", false) diff --git a/network/transport/grpc/connection_manager_test.go b/network/transport/grpc/connection_manager_test.go index e9b1e56f7c..f5aa5e2144 100644 --- a/network/transport/grpc/connection_manager_test.go +++ b/network/transport/grpc/connection_manager_test.go @@ -493,7 +493,7 @@ func Test_grpcConnectionManager_Peers(t *testing.T) { }, time.Second*2, "waiting for peer 1 to connect") }) t.Run("outbound stream triggers observer", func(t *testing.T) { - _, authenticator1, _, listener := create(t) + cm1, authenticator1, _, listener := create(t) cm2, authenticator2, _, _ := create(t, withBufconnDialer(listener)) authenticator1.EXPECT().Authenticate(*nodeDID, gomock.Any()).Return(transport.Peer{ID: "1"}, nil) authenticator2.EXPECT().Authenticate(*nodeDID, gomock.Any()).Return(transport.Peer{ID: "2"}, nil) @@ -506,9 +506,11 @@ func Test_grpcConnectionManager_Peers(t *testing.T) { cm2.Connect("bufnet", *nodeDID, nil) + // Wait until both sides have authenticated, otherwise the mock controller may + // run Finish() before authenticator1 fires and report a missing call. test.WaitFor(t, func() (bool, error) { - return capturedPeer.Load() != nil, nil - }, time.Second*2, "waiting for peer 2 observers") + return capturedPeer.Load() != nil && len(cm1.Peers()) > 0, nil + }, time.Second*2, "waiting for both sides to authenticate") assert.Equal(t, transport.Peer{ID: "2"}, capturedPeer.Load()) assert.Equal(t, transport.StateConnected, capturedState.Load()) @@ -532,9 +534,11 @@ func Test_grpcConnectionManager_Peers(t *testing.T) { cm2.Connect("bufnet", *nodeDID, nil) + // Wait until both sides have authenticated, otherwise the mock controller may + // run Finish() before authenticator2 fires and report a missing call. test.WaitFor(t, func() (bool, error) { - return capturedPeer.Load() != nil, nil - }, time.Second*2, "waiting for peer 1 observers") + return capturedPeer.Load() != nil && len(cm2.Peers()) > 0, nil + }, time.Second*2, "waiting for both sides to authenticate") assert.Equal(t, transport.Peer{ID: "1"}, capturedPeer.Load()) assert.Equal(t, transport.StateConnected, capturedState.Load()) diff --git a/network/transport/grpc/connection_test.go b/network/transport/grpc/connection_test.go index 101fda5b0f..1ca7652176 100644 --- a/network/transport/grpc/connection_test.go +++ b/network/transport/grpc/connection_test.go @@ -20,8 +20,11 @@ package grpc import ( "context" + "github.com/nuts-foundation/nuts-node/test" "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" "sync" + "sync/atomic" "testing" "time" @@ -101,6 +104,33 @@ func Test_conn_registerStream(t *testing.T) { }) } +func Test_conn_startSending(t *testing.T) { + t.Run("disconnect does not panic", func(t *testing.T) { + connection := createConnection(context.Background(), transport.Peer{}).(*conn) + stream := newServerStream("foo", "", nil) + + defer stream.cancelFunc() + + p := &TestProtocol{} + _ = connection.registerStream(p, stream) + + assert.Equal(t, int32(2), connection.activeGoroutines) // startSending and startReceiving + + // Disconnect before cancelling the stream: this guarantees the connection context is + // cancelled before RecvMsg returns, so the receive loop drops the message instead of + // racing to store the stream error as close status. + connection.disconnect() + stream.cancelFunc() + + test.WaitFor(t, func() (bool, error) { + return atomic.LoadInt32(&connection.activeGoroutines) == 0, nil + }, 5*time.Second, "waiting for all goroutines to exit") + + // A deliberate local disconnect must not record a close error. Default value is OK. + assert.Equal(t, codes.OK, connection.status.Load().Code()) + }) +} + func TestConn_Send(t *testing.T) { t.Run("buffer overflow softlimit", func(t *testing.T) { connection := createConnection(context.Background(), transport.Peer{}).(*conn) diff --git a/network/transport/v2/transactionlist_handler_test.go b/network/transport/v2/transactionlist_handler_test.go index 0971900cbc..a3df827136 100644 --- a/network/transport/v2/transactionlist_handler_test.go +++ b/network/transport/v2/transactionlist_handler_test.go @@ -195,7 +195,7 @@ func TestProtocol_handleTransactionList(t *testing.T) { }, }}) - assert.EqualError(t, err, "received transaction is invalid: unable to parse transaction: invalid compact serialization format: invalid number of segments") + assert.EqualError(t, err, "received transaction is invalid: unable to parse transaction: jws.Parse: failed to parse compact format: jws.Parse: invalid compact serialization format: jwsbb: invalid number of segments") }) t.Run("error - unknown conversationID", func(t *testing.T) { diff --git a/pki/denylist.go b/pki/denylist.go index e1a3039fb1..548572eb2f 100644 --- a/pki/denylist.go +++ b/pki/denylist.go @@ -29,9 +29,9 @@ import ( "sync/atomic" "time" - "github.com/lestrrat-go/jwx/v2/jwa" - "github.com/lestrrat-go/jwx/v2/jwk" - "github.com/lestrrat-go/jwx/v2/jws" + "github.com/lestrrat-go/jwx/v3/jwa" + "github.com/lestrrat-go/jwx/v3/jwk" + "github.com/lestrrat-go/jwx/v3/jws" ) // denylistImpl implements arbitrary certificate rejection using issuer and serial number tuples @@ -182,7 +182,7 @@ func (b *denylistImpl) Update() error { } // Check the signature of the denylist - payload, err := jws.Verify(bytes, jws.WithKey(jwa.EdDSA, b.trustedKey)) + payload, err := jws.Verify(bytes, jws.WithKey(jwa.EdDSA(), b.trustedKey)) if err != nil { return fmt.Errorf("failed to verify denylist signature: %w", err) } @@ -251,11 +251,11 @@ func certKeyJWKThumbprint(cert *x509.Certificate) string { // Compute the fingerprint of the key _ = jwk.AssignKeyID(key) - // Retrieve the fingerprint, which annoyingly is an "any" return type - fingerprint, _ := key.Get(jwk.KeyIDKey) + // Retrieve the fingerprint + var fingerprint string + _ = key.Get(jwk.KeyIDKey, &fingerprint) - // Use fmt to "convert" the fingerprint to a string - return fmt.Sprintf("%s", fingerprint) + return fingerprint } // If something above failed, default to an empty string. This would happen if the JWK library could not diff --git a/pki/denylist_test.go b/pki/denylist_test.go index 2b6e9bea27..00fbf4c340 100644 --- a/pki/denylist_test.go +++ b/pki/denylist_test.go @@ -31,9 +31,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/lestrrat-go/jwx/v2/jwa" - "github.com/lestrrat-go/jwx/v2/jwk" - "github.com/lestrrat-go/jwx/v2/jws" + "github.com/lestrrat-go/jwx/v3/jwa" + "github.com/lestrrat-go/jwx/v3/jwk" + "github.com/lestrrat-go/jwx/v3/jws" ) // Do not use this public key for anything other than unit tests in denylist_test.go @@ -179,7 +179,7 @@ func encodeDenylist(t *testing.T, entries []denylistEntry) string { require.NoError(t, err) // Sign the denylist as a JWS Message - compactJWS, err := jws.Sign(payload, jws.WithKey(jwa.EdDSA, key)) + compactJWS, err := jws.Sign(payload, jws.WithKey(jwa.EdDSA(), key)) require.NoError(t, err) // Return the compact encoded JWS message @@ -201,7 +201,7 @@ func TestNewDenylist(t *testing.T) { URL: "example.com", TrustedSigner: "definitely not valid", }) - assert.EqualError(t, err, "failed to parse key: failed to parse PEM encoded key: failed to decode PEM data") + assert.EqualError(t, err, "failed to parse key: jwk.ParseKey: failed to decode X.509 encoded key: failed to decode PEM data") }) } @@ -255,7 +255,7 @@ func TestDenylistMissing(t *testing.T) { // Sign an invalid denylist as a JWS Message payload := []byte("invalid payload") - compactJWS, err := jws.Sign(payload, jws.WithKey(jwa.EdDSA, key)) + compactJWS, err := jws.Sign(payload, jws.WithKey(jwa.EdDSA(), key)) require.NoError(t, err) // Setup a denylist server diff --git a/policy/local.go b/policy/local.go index 81719eb7d8..66e462b1ee 100644 --- a/policy/local.go +++ b/policy/local.go @@ -87,22 +87,14 @@ func (b *LocalPDP) PresentationDefinitions(_ context.Context, scope string) (pe. return result, nil } -// loadFromDirectory traverses all .json files in the given directory and loads them +// loadFromDirectory traverses all .json files in the given directory and loads them. +// Entries are processed in lexical order so duplicate-scope detection is deterministic. func (b *LocalPDP) loadFromDirectory(directory string) error { - // open the directory - dir, err := os.Open(directory) + files, err := os.ReadDir(directory) if err != nil { return err } - defer dir.Close() - // read all the files in the directory - files, err := dir.Readdir(0) - if err != nil { - return err - } - - // load all the files for _, file := range files { if file.IsDir() { continue diff --git a/test/pki/test.go b/test/pki/test.go index f0ee9de573..021de6d8a5 100644 --- a/test/pki/test.go +++ b/test/pki/test.go @@ -27,7 +27,7 @@ import ( _ "embed" "encoding/asn1" "encoding/base64" - "github.com/lestrrat-go/jwx/v2/cert" + "github.com/lestrrat-go/jwx/v3/cert" "github.com/nuts-foundation/nuts-node/test/io" "math/big" "net" diff --git a/vcr/credential/resolver_test.go b/vcr/credential/resolver_test.go index f2fb94f18a..504da44bd8 100644 --- a/vcr/credential/resolver_test.go +++ b/vcr/credential/resolver_test.go @@ -23,9 +23,9 @@ import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" - "github.com/lestrrat-go/jwx/v2/jwa" - "github.com/lestrrat-go/jwx/v2/jws" - "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/lestrrat-go/jwx/v3/jwa" + "github.com/lestrrat-go/jwx/v3/jws" + "github.com/lestrrat-go/jwx/v3/jwt" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/nuts-node/vcr/signature/proof" "github.com/nuts-foundation/nuts-node/vcr/test" @@ -73,7 +73,7 @@ func TestPresentationSigner(t *testing.T) { t.Run("ok", func(t *testing.T) { headers := jws.NewHeaders() headers.Set(jws.KeyIDKey, keyID.String()) - signedToken, err := jwt.Sign(jwt.New(), jwt.WithKey(jwa.ES256, privateKey, jws.WithProtectedHeaders(headers))) + signedToken, err := jwt.Sign(jwt.New(), jwt.WithKey(jwa.ES256(), privateKey, jws.WithProtectedHeaders(headers))) require.NoError(t, err) presentation, err := vc.ParseVerifiablePresentation(string(signedToken)) require.NoError(t, err) @@ -83,7 +83,7 @@ func TestPresentationSigner(t *testing.T) { assert.Equal(t, keyID.DID, *actual) }) t.Run("no kid header", func(t *testing.T) { - signedToken, err := jwt.Sign(jwt.New(), jwt.WithKey(jwa.ES256, privateKey)) + signedToken, err := jwt.Sign(jwt.New(), jwt.WithKey(jwa.ES256(), privateKey)) require.NoError(t, err) presentation, err := vc.ParseVerifiablePresentation(string(signedToken)) require.NoError(t, err) @@ -96,7 +96,7 @@ func TestPresentationSigner(t *testing.T) { t.Run("kid is not a did", func(t *testing.T) { headers := jws.NewHeaders() require.NoError(t, headers.Set(jws.KeyIDKey, "not a did")) - signedToken, err := jwt.Sign(jwt.New(), jwt.WithKey(jwa.ES256, privateKey, jws.WithProtectedHeaders(headers))) + signedToken, err := jwt.Sign(jwt.New(), jwt.WithKey(jwa.ES256(), privateKey, jws.WithProtectedHeaders(headers))) require.NoError(t, err) presentation, err := vc.ParseVerifiablePresentation(string(signedToken)) require.NoError(t, err) diff --git a/vcr/credential/util.go b/vcr/credential/util.go index 41643548f7..a24b8f4b51 100644 --- a/vcr/credential/util.go +++ b/vcr/credential/util.go @@ -74,8 +74,9 @@ func PresentationIssuanceDate(presentation vc.VerifiablePresentation) *time.Time switch presentation.Format() { case vc.JWTPresentationProofFormat: jwt := presentation.JWT() - if result = jwt.NotBefore(); result.IsZero() { - result = jwt.IssuedAt() + nbf, _ := jwt.NotBefore() + if result = nbf; result.IsZero() { + result, _ = jwt.IssuedAt() } case vc.JSONLDPresentationProofFormat: ldProof, err := ParseLDProof(presentation) @@ -98,7 +99,7 @@ func PresentationExpirationDate(presentation vc.VerifiablePresentation) *time.Ti var result time.Time switch presentation.Format() { case vc.JWTPresentationProofFormat: - result = presentation.JWT().Expiration() + result, _ = presentation.JWT().Expiration() case vc.JSONLDPresentationProofFormat: ldProof, err := ParseLDProof(presentation) if err != nil || ldProof.Expires == nil { diff --git a/vcr/credential/util_test.go b/vcr/credential/util_test.go index c84ccafa6d..06d016c7fb 100644 --- a/vcr/credential/util_test.go +++ b/vcr/credential/util_test.go @@ -19,7 +19,7 @@ package credential import ( - "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/lestrrat-go/jwx/v3/jwt" ssi "github.com/nuts-foundation/go-did" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/go-did/vc" diff --git a/vcr/credential/validator.go b/vcr/credential/validator.go index ac2b481dae..8ae00f2cab 100644 --- a/vcr/credential/validator.go +++ b/vcr/credential/validator.go @@ -25,7 +25,7 @@ import ( "encoding/json" "errors" "fmt" - "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v3/jwk" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/go-did/vc" "github.com/nuts-foundation/nuts-node/crypto" diff --git a/vcr/credential/validator_test.go b/vcr/credential/validator_test.go index acc926e757..a6c2659fe7 100644 --- a/vcr/credential/validator_test.go +++ b/vcr/credential/validator_test.go @@ -25,7 +25,7 @@ import ( "testing" "time" - "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/lestrrat-go/jwx/v3/jwt" ssi "github.com/nuts-foundation/go-did" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/go-did/vc" @@ -575,12 +575,17 @@ func TestX509CredentialValidator_Validate(t *testing.T) { x509credential := test.ValidX509Credential(t, func(builder *jwt.Builder) *jwt.Builder { // Build new jwt.Builder without expiration token, _ := builder.Build() - vc, _ := token.Get("vc") + var vc interface{} + _ = token.Get("vc", &vc) + nbf, _ := token.NotBefore() + sub, _ := token.Subject() + iss, _ := token.Issuer() + jti, _ := token.JwtID() return jwt.NewBuilder(). - NotBefore(token.NotBefore()). - Subject(token.Subject()). - Issuer(token.Issuer()). - JwtID(token.JwtID()). + NotBefore(nbf). + Subject(sub). + Issuer(iss). + JwtID(jti). Claim("vc", vc) }) diff --git a/vcr/holder/presenter.go b/vcr/holder/presenter.go index 182f434d41..e77c456d4a 100644 --- a/vcr/holder/presenter.go +++ b/vcr/holder/presenter.go @@ -24,8 +24,8 @@ import ( "errors" "fmt" "github.com/google/uuid" - "github.com/lestrrat-go/jwx/v2/jws" - "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/lestrrat-go/jwx/v3/jws" + "github.com/lestrrat-go/jwx/v3/jwt" ssi "github.com/nuts-foundation/go-did" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/go-did/vc" diff --git a/vcr/holder/presenter_test.go b/vcr/holder/presenter_test.go index efc47e70b8..9d6805ebdc 100644 --- a/vcr/holder/presenter_test.go +++ b/vcr/holder/presenter_test.go @@ -160,9 +160,11 @@ func TestPresenter_buildPresentation(t *testing.T) { assert.NotEmpty(t, result.ID.Fragment, "id must have a fragment") assert.Equal(t, JWTPresentationFormat, result.Format()) assert.NotNil(t, result.JWT()) - nonce, _ := result.JWT().Get("nonce") + var nonce interface{} + _ = result.JWT().Get("nonce", &nonce) assert.Empty(t, nonce) }) + t.Run("ok - multiple VCs", func(t *testing.T) { ctrl := gomock.NewController(t) @@ -209,12 +211,17 @@ func TestPresenter_buildPresentation(t *testing.T) { require.NotNil(t, result) assert.Equal(t, JWTPresentationFormat, result.Format()) assert.NotNil(t, result.JWT()) - assert.Equal(t, *options.ProofOptions.Expires, result.JWT().Expiration().Local()) - assert.Equal(t, options.ProofOptions.Created, result.JWT().NotBefore().Local()) - assert.Equal(t, []string{domain}, result.JWT().Audience()) - actualNonce, _ := result.JWT().Get("nonce") + expiration, _ := result.JWT().Expiration() + assert.Equal(t, *options.ProofOptions.Expires, expiration.Local()) + notBefore, _ := result.JWT().NotBefore() + assert.Equal(t, options.ProofOptions.Created, notBefore.Local()) + audience, _ := result.JWT().Audience() + assert.Equal(t, []string{domain}, audience) + var actualNonce interface{} + _ = result.JWT().Get("nonce", &actualNonce) assert.Equal(t, nonce, actualNonce) - actualCustomClaim, _ := result.JWT().Get("custom") + var actualCustomClaim interface{} + _ = result.JWT().Get("custom", &actualCustomClaim) assert.Equal(t, "claim", actualCustomClaim) }) }) diff --git a/vcr/issuer/issuer_test.go b/vcr/issuer/issuer_test.go index 2cfc4d2272..36a038fea9 100644 --- a/vcr/issuer/issuer_test.go +++ b/vcr/issuer/issuer_test.go @@ -143,10 +143,14 @@ func Test_issuer_buildAndSignVC(t *testing.T) { assert.Empty(t, result.Proof) // Assert JWT require.NotNil(t, result.JWT()) - assert.Equal(t, subjectDID, result.JWT().Subject()) - assert.Equal(t, result.IssuanceDate, result.JWT().NotBefore()) - assert.Equal(t, *result.ExpirationDate, result.JWT().Expiration()) - assert.Equal(t, result.ID.String(), result.JWT().JwtID()) + subject, _ := result.JWT().Subject() + assert.Equal(t, subjectDID, subject) + notBefore, _ := result.JWT().NotBefore() + assert.Equal(t, result.IssuanceDate, notBefore) + expiration, _ := result.JWT().Expiration() + assert.Equal(t, *result.ExpirationDate, expiration) + jwtID, _ := result.JWT().JwtID() + assert.Equal(t, result.ID.String(), jwtID) }) }) t.Run("credentialStatus", func(t *testing.T) { diff --git a/vcr/issuer/openid.go b/vcr/issuer/openid.go index 423cb0cf03..c82d47287e 100644 --- a/vcr/issuer/openid.go +++ b/vcr/issuer/openid.go @@ -25,8 +25,8 @@ import ( "errors" "fmt" "github.com/google/uuid" - "github.com/lestrrat-go/jwx/v2/jws" - "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/lestrrat-go/jwx/v3/jws" + "github.com/lestrrat-go/jwx/v3/jwt" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/go-did/vc" "github.com/nuts-foundation/nuts-node/audit" @@ -326,8 +326,9 @@ func (i *openidHandler) validateProof(ctx context.Context, flow *Flow, request o } // Validate audience + audience, _ := token.Audience() audienceMatches := false - for _, aud := range token.Audience() { + for _, aud := range audience { if aud == i.issuerIdentifierURL { audienceMatches = true break @@ -335,7 +336,7 @@ func (i *openidHandler) validateProof(ctx context.Context, flow *Flow, request o } if !audienceMatches { return generateProofError(openid4vci.Error{ - Err: fmt.Errorf("audience doesn't match credential issuer (aud=%s)", token.Audience()), + Err: fmt.Errorf("audience doesn't match credential issuer (aud=%s)", audience), Code: openid4vci.InvalidProof, StatusCode: http.StatusBadRequest, }) @@ -352,7 +353,7 @@ func (i *openidHandler) validateProof(ctx context.Context, flow *Flow, request o // I think this is impossible return errors.New("expected exactly one signature") } - typ := message.Signatures()[0].ProtectedHeaders().Type() + typ, _ := message.Signatures()[0].ProtectedHeaders().Type() if typ == "" { return generateProofError(openid4vci.Error{ Err: errors.New("missing typ header"), @@ -369,8 +370,8 @@ func (i *openidHandler) validateProof(ctx context.Context, flow *Flow, request o } // given the JWT typ, the nonce is in the 'nonce' claim - nonce, ok := token.Get("nonce") - if !ok { + var nonce string + if err := token.Get("nonce", &nonce); err != nil { return generateProofError(openid4vci.Error{ Err: errors.New("missing nonce claim"), Code: openid4vci.InvalidProof, @@ -379,7 +380,7 @@ func (i *openidHandler) validateProof(ctx context.Context, flow *Flow, request o } // check if the nonce matches the one we sent in the offer - flowFromNonce, err := i.store.FindByReference(ctx, cNonceRefType, nonce.(string)) + flowFromNonce, err := i.store.FindByReference(ctx, cNonceRefType, nonce) if err != nil { return err } diff --git a/vcr/issuer/openid_test.go b/vcr/issuer/openid_test.go index f81e25baca..562b1ec940 100644 --- a/vcr/issuer/openid_test.go +++ b/vcr/issuer/openid_test.go @@ -234,7 +234,7 @@ func Test_memoryIssuer_HandleCredentialRequest(t *testing.T) { response, err := service.HandleCredentialRequest(ctx, invalidRequest, accessToken) - assertProtocolError(t, err, http.StatusBadRequest, "invalid_proof - invalid compact serialization format: invalid number of segments") + assertProtocolError(t, err, http.StatusBadRequest, "invalid_proof - jws.ParseString: failed to parse string: jws.Parse: failed to parse compact format: jws.Parse: invalid compact serialization format: jwsbb: invalid number of segments") assert.Nil(t, response) }) t.Run("not signed by intended wallet (DID differs)", func(t *testing.T) { diff --git a/vcr/pe/presentation_definition.go b/vcr/pe/presentation_definition.go index df41b6907f..b4d1ca8c66 100644 --- a/vcr/pe/presentation_definition.go +++ b/vcr/pe/presentation_definition.go @@ -22,8 +22,7 @@ import ( "encoding/json" "errors" "fmt" - "github.com/lestrrat-go/jwx/v2/jwa" - "github.com/lestrrat-go/jwx/v2/jws" + "github.com/lestrrat-go/jwx/v3/jws" v2 "github.com/nuts-foundation/nuts-node/vcr/pe/schema/v2" "strings" @@ -328,7 +327,7 @@ func matchFormat(format *PresentationDefinitionClaimFormatDesignations, credenti case vc.JWTCredentialProofFormat: // Get signing algorithm used to sign the JWT message, _ := jws.ParseString(credential.Raw()) // can't really fail, JWT has been parsed before. - signingAlgorithm, _ := message.Signatures()[0].ProtectedHeaders().Get(jws.AlgorithmKey) + signingAlgorithm, _ := message.Signatures()[0].ProtectedHeaders().Algorithm() // Check that the signing algorithm is specified by the presentation definition if entry := asMap[vc.JWTCredentialProofFormat]; entry != nil { if len(message.Signatures()[0].Signature()) == 0 { @@ -337,7 +336,7 @@ func matchFormat(format *PresentationDefinitionClaimFormatDesignations, credenti } if supportedAlgorithms := entry[jws.AlgorithmKey]; supportedAlgorithms != nil { for _, supportedAlgorithm := range supportedAlgorithms { - if signingAlgorithm == jwa.SignatureAlgorithm(supportedAlgorithm) { + if signingAlgorithm.String() == supportedAlgorithm { return true } } diff --git a/vcr/pe/presentation_definition_test.go b/vcr/pe/presentation_definition_test.go index 9c220c3bee..d96f626732 100644 --- a/vcr/pe/presentation_definition_test.go +++ b/vcr/pe/presentation_definition_test.go @@ -23,6 +23,7 @@ import ( "crypto/elliptic" "crypto/rand" "embed" + "encoding/base64" "encoding/json" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/nuts-node/core/to" @@ -30,8 +31,8 @@ import ( "strings" "testing" - "github.com/lestrrat-go/jwx/v2/jwa" - "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/lestrrat-go/jwx/v3/jwa" + "github.com/lestrrat-go/jwx/v3/jwt" ssi "github.com/nuts-foundation/go-did" "github.com/nuts-foundation/go-did/vc" "github.com/nuts-foundation/nuts-node/vcr/pe/test" @@ -221,7 +222,7 @@ func TestMatch(t *testing.T) { t.Run("unsupported JOSE alg", func(t *testing.T) { token := jwt.New() privateKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - signedToken, err := jwt.Sign(token, jwt.WithKey(jwa.ES256, privateKey)) + signedToken, err := jwt.Sign(token, jwt.WithKey(jwa.ES256(), privateKey)) require.NoError(t, err) jwtCredential, err := vc.ParseVerifiableCredential(string(signedToken)) require.NoError(t, err) @@ -513,8 +514,13 @@ txJy6M1-lD7a5HTzanYTWBPAUHDZGyGKXdJw-W_x0IWChBzI8t3kpG253fg6V3tPgHeKXE94fz_QpYfg assert.False(t, match) }) t.Run("no proof in credential (self-attested)", func(t *testing.T) { + // A self-attested credential carries no signature. In jwx v3 an empty compact + // signature is only valid when the protected header declares "alg":"none", so + // build the unsigned credential with a none-alg header (jwx v2 tolerated dropping + // the signature from a signed header, jwx v3 does not). jwtCredNoProof := strings.Split(jwtCredential, ".") - credNoProof, err := vc.ParseVerifiableCredential(jwtCredNoProof[0] + "." + jwtCredNoProof[1] + ".") + noneHeader := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"none","typ":"JWT"}`)) + credNoProof, err := vc.ParseVerifiableCredential(noneHeader + "." + jwtCredNoProof[1] + ".") require.NoError(t, err) match := matchFormat(&PresentationDefinitionClaimFormatDesignations{ "jwt_vc": {"alg": {"ES521"}}, diff --git a/vcr/pe/util.go b/vcr/pe/util.go index 75ced72c07..c6817bb879 100644 --- a/vcr/pe/util.go +++ b/vcr/pe/util.go @@ -21,7 +21,7 @@ package pe import ( "encoding/json" "fmt" - "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/lestrrat-go/jwx/v3/jwt" "github.com/nuts-foundation/go-did/vc" ) @@ -139,11 +139,13 @@ func parseJSONObjectOrStringEnvelope(envelopeBytes []byte) (interface{}, *vc.Ver } asMap := make(map[string]interface{}) // use the 'vp' claim as base Verifiable Presentation properties - innerVPAsMap, _ := token.PrivateClaims()["vp"].(map[string]interface{}) + var innerVP interface{} + _ = token.Get("vp", &innerVP) + innerVPAsMap, _ := innerVP.(map[string]interface{}) for key, value := range innerVPAsMap { asMap[key] = value } - if jti, ok := token.Get(jwt.JwtIDKey); ok { + if jti, ok := token.JwtID(); ok { asMap["id"] = jti } return asMap, presentation, nil diff --git a/vcr/pe/util_test.go b/vcr/pe/util_test.go index 999df47fac..d9579dd0e9 100644 --- a/vcr/pe/util_test.go +++ b/vcr/pe/util_test.go @@ -37,7 +37,7 @@ func TestParseEnvelope(t *testing.T) { }) t.Run("invalid JWT", func(t *testing.T) { envelope, err := ParseEnvelope([]byte(`eyINVALID`)) - assert.EqualError(t, err, "unable to parse PEX envelope as verifiable presentation: invalid JWT") + assert.EqualError(t, err, "unable to parse PEX envelope as verifiable presentation: jwt.Parse: failed to parse token: unknown payload type (payload is not JWT?)") assert.Nil(t, envelope) }) t.Run("JSON object", func(t *testing.T) { @@ -73,7 +73,7 @@ func TestParseEnvelope(t *testing.T) { }) t.Run("invalid format", func(t *testing.T) { envelope, err := ParseEnvelope([]byte(`true`)) - assert.EqualError(t, err, "unable to parse PEX envelope as verifiable presentation: invalid JWT") + assert.EqualError(t, err, "unable to parse PEX envelope as verifiable presentation: jwt.Parse: failed to parse token: unknown payload type (payload is not JWT?)") assert.Nil(t, envelope) }) } diff --git a/vcr/signature/json_web_signature.go b/vcr/signature/json_web_signature.go index b3eeceaed0..8d3bcc2033 100644 --- a/vcr/signature/json_web_signature.go +++ b/vcr/signature/json_web_signature.go @@ -21,7 +21,7 @@ package signature import ( "context" "fmt" - "github.com/lestrrat-go/jwx/v2/jws" + "github.com/lestrrat-go/jwx/v3/jws" ssi "github.com/nuts-foundation/go-did" "github.com/nuts-foundation/nuts-node/crypto" "github.com/nuts-foundation/nuts-node/crypto/hash" diff --git a/vcr/signature/json_web_signature_test.go b/vcr/signature/json_web_signature_test.go index 541b4be01d..6f5591dde3 100644 --- a/vcr/signature/json_web_signature_test.go +++ b/vcr/signature/json_web_signature_test.go @@ -19,10 +19,9 @@ package signature import ( - "context" "encoding/hex" - "github.com/lestrrat-go/jwx/v2/jwa" - "github.com/lestrrat-go/jwx/v2/jws" + "github.com/lestrrat-go/jwx/v3/jwa" + "github.com/lestrrat-go/jwx/v3/jws" ssi "github.com/nuts-foundation/go-did" "github.com/nuts-foundation/nuts-node/audit" "github.com/nuts-foundation/nuts-node/crypto" @@ -133,12 +132,18 @@ func TestJsonWebSignature2020_Sign(t *testing.T) { assert.Empty(t, msg.Payload(), "payload should be empty (detached)") require.Len(t, msg.Signatures(), 1) expectedHeaders := map[string]interface{}{ - "alg": jwa.ES256, + "alg": jwa.ES256(), "b64": false, "crit": []string{"b64"}, "kid": keyID, } - actualHeaders, _ := msg.Signatures()[0].ProtectedHeaders().AsMap(context.Background()) + protected := msg.Signatures()[0].ProtectedHeaders() + actualHeaders := make(map[string]interface{}) + for _, k := range protected.Keys() { + var v interface{} + _ = protected.Get(k, &v) + actualHeaders[k] = v + } assert.Equal(t, expectedHeaders, actualHeaders) }) } diff --git a/vcr/signature/proof/jsonld.go b/vcr/signature/proof/jsonld.go index 93f78a1fae..b91f620afb 100644 --- a/vcr/signature/proof/jsonld.go +++ b/vcr/signature/proof/jsonld.go @@ -29,7 +29,7 @@ import ( "strings" "time" - "github.com/lestrrat-go/jwx/v2/jws" + "github.com/lestrrat-go/jwx/v3/jws" ssi "github.com/nuts-foundation/go-did" nutsCrypto "github.com/nuts-foundation/nuts-node/crypto" "github.com/nuts-foundation/nuts-node/vcr/signature" @@ -131,7 +131,6 @@ func (p LDProof) Verify(document Document, suite signature.Suite, key crypto.Pub return err } - jswVerifier, _ := jws.NewVerifier(alg) // the jws lib can't do this for us, so we concat hdr with payload for verification splittedJws := strings.Split(p.JWS, "..") if len(splittedJws) != 2 { @@ -142,7 +141,11 @@ func (p LDProof) Verify(document Document, suite signature.Suite, key crypto.Pub return fmt.Errorf("could not base64 decode signature: %w", err) } challenge := fmt.Sprintf("%s.%s", splittedJws[0], tbv) - if err = jswVerifier.Verify([]byte(challenge), sig, key); err != nil { + verifier, err := jws.VerifierFor(alg) + if err != nil { + return err + } + if err = verifier.Verify(key, []byte(challenge), sig); err != nil { return fmt.Errorf("invalid proof signature: %w", err) } return nil diff --git a/vcr/signature/proof/jsonld_test.go b/vcr/signature/proof/jsonld_test.go index 04ed3a940f..cc4d807e9d 100644 --- a/vcr/signature/proof/jsonld_test.go +++ b/vcr/signature/proof/jsonld_test.go @@ -153,7 +153,7 @@ func TestLDProof_Verify(t *testing.T) { // signature with some changed characters ldProof.JWS = "eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..MJ5GwWRMsadCyLNXU_flgJtsS32584MydBxBuyups_cM0sbU3abTEOMyUvmLNcKOwOBE1MfDoB1_YY425W3sAg" err = ldProof.Verify(signedDocument.DocumentWithoutProof(), signature.JSONWebSignature2020{ContextLoader: contextLoader}, pk) - assert.EqualError(t, err, "invalid proof signature: failed to match EdDSA signature") + assert.EqualError(t, err, "invalid proof signature: invalid EdDSA signature") }) } diff --git a/vcr/store_test.go b/vcr/store_test.go index ebdb9fc0b9..d6e10048f1 100644 --- a/vcr/store_test.go +++ b/vcr/store_test.go @@ -24,6 +24,7 @@ import ( "crypto/ecdsa" "crypto/sha1" "encoding/json" + "github.com/lestrrat-go/jwx/v3/jwk" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/nuts-node/crypto/storage/spi" "github.com/nuts-foundation/nuts-node/vcr/test" @@ -48,7 +49,7 @@ func TestVcr_StoreCredential(t *testing.T) { pkeJSON, _ := os.ReadFile("test/public.json") json.Unmarshal(pkeJSON, &pke) var pk = new(ecdsa.PublicKey) - pke.JWK().Raw(pk) + jwk.Export(pke.JWK(), pk) t.Run("ok - not owned, do not store in wallet", func(t *testing.T) { ctx := newMockContext(t) diff --git a/vcr/test/credentials.go b/vcr/test/credentials.go index f45572c7cd..e405589000 100644 --- a/vcr/test/credentials.go +++ b/vcr/test/credentials.go @@ -27,10 +27,10 @@ import ( "encoding/base64" "encoding/json" "fmt" - "github.com/lestrrat-go/jwx/v2/cert" - "github.com/lestrrat-go/jwx/v2/jwa" - "github.com/lestrrat-go/jwx/v2/jws" - "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/lestrrat-go/jwx/v3/cert" + "github.com/lestrrat-go/jwx/v3/jwa" + "github.com/lestrrat-go/jwx/v3/jws" + "github.com/lestrrat-go/jwx/v3/jwt" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/nuts-node/test/pki" "github.com/nuts-foundation/nuts-node/vcr/assets" @@ -99,7 +99,7 @@ func JWTNutsOrganizationCredential(t *testing.T, subjectID did.DID) vc.Verifiabl }, "type": "NutsOrganizationCredential", })) - signedToken, err := jwt.Sign(token, jwt.WithKey(jwa.ES384, privateKey)) + signedToken, err := jwt.Sign(token, jwt.WithKey(jwa.ES384(), privateKey)) require.NoError(t, err) jwtVC, err := vc.ParseVerifiableCredential(string(signedToken)) require.NoError(t, err) @@ -180,7 +180,7 @@ func ValidX509Credential(t *testing.T, options ...credentialOption) vc.Verifiabl } token, err := builder.Build() require.NoError(t, err) - s, err := jwt.Sign(token, jwt.WithKey(jwa.RS256, rootKey, jws.WithProtectedHeaders(headers))) + s, err := jwt.Sign(token, jwt.WithKey(jwa.RS256(), rootKey, jws.WithProtectedHeaders(headers))) require.NoError(t, err) credential, err := vc.ParseVerifiableCredential(string(s)) require.NoError(t, err) diff --git a/vcr/test/test.go b/vcr/test/test.go index 8b8c732cc7..a27b4030ee 100644 --- a/vcr/test/test.go +++ b/vcr/test/test.go @@ -19,16 +19,16 @@ package test import ( - "context" "crypto" "github.com/google/uuid" - "github.com/lestrrat-go/jwx/v2/jws" - "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/lestrrat-go/jwx/v3/jws" + "github.com/lestrrat-go/jwx/v3/jwt" ssi "github.com/nuts-foundation/go-did" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/go-did/vc" "github.com/nuts-foundation/nuts-node/audit" nutsCrypto "github.com/nuts-foundation/nuts-node/crypto" + "github.com/nuts-foundation/nuts-node/crypto/jwx" "github.com/nuts-foundation/nuts-node/vcr/signature/proof" "github.com/stretchr/testify/require" "testing" @@ -60,7 +60,8 @@ func CreateJWTPresentation(t *testing.T, subjectDID did.DID, tokenVisitor func(t keyStore := nutsCrypto.NewMemoryCryptoInstance(t) _, key, err := keyStore.New(audit.TestContext(), nutsCrypto.StringNamingFunc(kid)) require.NoError(t, err) - claims, err = unsignedToken.AsMap(context.Background()) + claims, err = jwx.ClaimsAsMap(unsignedToken) + require.NoError(t, err) signedToken, err := keyStore.SignJWT(audit.TestContext(), claims, headers, kid) result, err := vc.ParseVerifiablePresentation(signedToken) require.NoError(t, err) diff --git a/vcr/verifier/signature_verifier.go b/vcr/verifier/signature_verifier.go index 3ff787656e..e0390148f3 100644 --- a/vcr/verifier/signature_verifier.go +++ b/vcr/verifier/signature_verifier.go @@ -25,7 +25,7 @@ import ( "strings" "time" - "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/lestrrat-go/jwx/v3/jwt" "github.com/nuts-foundation/go-did/vc" "github.com/nuts-foundation/nuts-node/crypto" "github.com/nuts-foundation/nuts-node/jsonld" diff --git a/vcr/verifier/signature_verifier_test.go b/vcr/verifier/signature_verifier_test.go index 38805b38ac..b91a7c084e 100644 --- a/vcr/verifier/signature_verifier_test.go +++ b/vcr/verifier/signature_verifier_test.go @@ -32,16 +32,17 @@ import ( "encoding/json" "errors" "os" + "strings" "testing" "time" "github.com/google/uuid" - "github.com/lestrrat-go/jwx/v2/cert" - "github.com/lestrrat-go/jwx/v2/jwa" + "github.com/lestrrat-go/jwx/v3/cert" + "github.com/lestrrat-go/jwx/v3/jwa" testpki "github.com/nuts-foundation/nuts-node/test/pki" "github.com/nuts-foundation/nuts-node/vdr/didx509" - "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v3/jwk" ssi "github.com/nuts-foundation/go-did" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/go-did/vc" @@ -64,7 +65,7 @@ func TestSignatureVerifier_VerifySignature(t *testing.T) { pkeJSON, _ := os.ReadFile("../test/public.json") json.Unmarshal(pkeJSON, &pke) var pk = new(ecdsa.PublicKey) - pke.JWK().Raw(pk) + jwk.Export(pke.JWK(), pk) t.Run("JSON-LD", func(t *testing.T) { t.Run("ok", func(t *testing.T) { @@ -119,7 +120,7 @@ func TestSignatureVerifier_VerifySignature(t *testing.T) { // Create did:jwk for issuer, and sign credential keyStore := nutsCrypto.NewMemoryCryptoInstance(t) kid, key, err := keyStore.New(audit.TestContext(), func(key crypto.PublicKey) (string, error) { - keyAsJWK, _ := jwk.FromRaw(key) + keyAsJWK, _ := jwk.Import(key) keyJSON, _ := json.Marshal(keyAsJWK) return "did:jwk:" + base64.RawStdEncoding.EncodeToString(keyJSON) + "#0", nil }) @@ -161,7 +162,7 @@ func TestSignatureVerifier_VerifySignature(t *testing.T) { err = sv.VerifySignature(*cred, nil) - assert.EqualError(t, err, "presentation(s) or credential(s) verification failed: unable to validate JWT signature: could not verify message using any of the signatures or keys") + assert.EqualError(t, err, "presentation(s) or credential(s) verification failed: unable to validate JWT signature: jwt.ParseString: failed to parse string: signature verification failed for ES256: invalid ECDSA signature") }) t.Run("expired token", func(t *testing.T) { // Credential taken from Sphereon Wallet, expires on Tue Oct 03 2023 @@ -175,7 +176,7 @@ func TestSignatureVerifier_VerifySignature(t *testing.T) { } err := sv.VerifySignature(*cred, nil) - assert.EqualError(t, err, "presentation(s) or credential(s) verification failed: unable to validate JWT signature: \"exp\" not satisfied") + assert.EqualError(t, err, "presentation(s) or credential(s) verification failed: unable to validate JWT signature: jwt.ParseString: failed to parse string: jwt.Validate: validation failed: \"exp\" not satisfied: token is expired") }) t.Run("without kid header, derived from issuer", func(t *testing.T) { // Credential taken from Sphereon Wallet @@ -195,16 +196,23 @@ func TestSignatureVerifier_VerifySignature(t *testing.T) { t.Run("no signature", func(t *testing.T) { // Credential taken from Sphereon Wallet const credentialJSON = `eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2OTYzMDE3MDgsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJHdWVzdENyZWRlbnRpYWwiXSwiY3JlZGVudGlhbFN1YmplY3QiOnsiZmlyc3ROYW1lIjoiSGVsbG8iLCJsYXN0TmFtZSI6IlNwaGVyZW9uIiwiZW1haWwiOiJzcGhlcmVvbkBleGFtcGxlLmNvbSIsInR5cGUiOiJTcGhlcmVvbiBHdWVzdCIsImlkIjoiZGlkOmp3azpleUpoYkdjaU9pSkZVekkxTmtzaUxDSjFjMlVpT2lKemFXY2lMQ0pyZEhraU9pSkZReUlzSW1OeWRpSTZJbk5sWTNBeU5UWnJNU0lzSW5naU9pSmpNVmRZY3pkWE0yMTVjMlZWWms1Q2NYTjRaRkJYUWtsSGFFdGtORlI2TUV4U0xVWnFPRVpOV1dFd0lpd2llU0k2SWxkdGEwTllkVEYzZVhwYVowZE9OMVY0VG1Gd2NIRnVUMUZoVDJ0WE1rTm5UMU51VDI5NVRVbFVkV01pZlEifX0sIkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJHdWVzdENyZWRlbnRpYWwiXSwiZXhwaXJhdGlvbkRhdGUiOiIyMDIzLTEwLTAzVDAyOjU1OjA4LjEzM1oiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJmaXJzdE5hbWUiOiJIZWxsbyIsImxhc3ROYW1lIjoiU3BoZXJlb24iLCJlbWFpbCI6InNwaGVyZW9uQGV4YW1wbGUuY29tIiwidHlwZSI6IlNwaGVyZW9uIEd1ZXN0IiwiaWQiOiJkaWQ6andrOmV5SmhiR2NpT2lKRlV6STFOa3NpTENKMWMyVWlPaUp6YVdjaUxDSnJkSGtpT2lKRlF5SXNJbU55ZGlJNkluTmxZM0F5TlRack1TSXNJbmdpT2lKak1WZFljemRYTTIxNWMyVlZaazVDY1hONFpGQlhRa2xIYUV0a05GUjZNRXhTTFVacU9FWk5XV0V3SWl3aWVTSTZJbGR0YTBOWWRURjNlWHBhWjBkT04xVjRUbUZ3Y0hGdVQxRmhUMnRYTWtOblQxTnVUMjk1VFVsVWRXTWlmUSJ9LCJpc3N1ZXIiOiJkaWQ6andrOmV5SmhiR2NpT2lKRlV6STFOaUlzSW5WelpTSTZJbk5wWnlJc0ltdDBlU0k2SWtWRElpd2lZM0oySWpvaVVDMHlOVFlpTENKNElqb2lWRWN5U0RKNE1tUlhXRTR6ZFVOeFduQnhSakY1YzBGUVVWWkVTa1ZPWDBndFEwMTBZbWRxWWkxT1p5SXNJbmtpT2lJNVRUaE9lR1F3VUU0eU1rMDViRkJFZUdSd1JIQnZWRXg2TVRWM1pubGFTbk0yV21oTFNWVktNek00SW4wIiwiaXNzdWFuY2VEYXRlIjoiMjAyMy0wOS0yOVQxMjozMTowOC4xMzNaIiwic3ViIjoiZGlkOmp3azpleUpoYkdjaU9pSkZVekkxTmtzaUxDSjFjMlVpT2lKemFXY2lMQ0pyZEhraU9pSkZReUlzSW1OeWRpSTZJbk5sWTNBeU5UWnJNU0lzSW5naU9pSmpNVmRZY3pkWE0yMTVjMlZWWms1Q2NYTjRaRkJYUWtsSGFFdGtORlI2TUV4U0xVWnFPRVpOV1dFd0lpd2llU0k2SWxkdGEwTllkVEYzZVhwYVowZE9OMVY0VG1Gd2NIRnVUMUZoVDJ0WE1rTm5UMU51VDI5NVRVbFVkV01pZlEiLCJuYmYiOjE2OTU5OTA2NjgsImlzcyI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGVXpJMU5pSXNJblZ6WlNJNkluTnBaeUlzSW10MGVTSTZJa1ZESWl3aVkzSjJJam9pVUMweU5UWWlMQ0o0SWpvaVZFY3lTREo0TW1SWFdFNHpkVU54V25CeFJqRjVjMEZRVVZaRVNrVk9YMGd0UTAxMFltZHFZaTFPWnlJc0lua2lPaUk1VFRoT2VHUXdVRTR5TWswNWJGQkVlR1J3UkhCdlZFeDZNVFYzWm5sYVNuTTJXbWhMU1ZWS016TTRJbjAifQ.` - cred, _ := vc.ParseVerifiableCredential(credentialJSON) + // A self-attested credential has no signature. In jwx v3 an empty compact signature + // is only accepted with an "alg":"none" protected header, so build the unsigned + // credential with a none-alg header (jwx v2 tolerated dropping the signature from a + // signed header, jwx v3 does not). Signature verification must still reject it. + parts := strings.Split(credentialJSON, ".") + noneHeader := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"none","typ":"JWT"}`)) + cred, err := vc.ParseVerifiableCredential(noneHeader + "." + parts[1] + ".") + require.NoError(t, err) sv := signatureVerifier{ keyResolver: resolver.DIDKeyResolver{ Resolver: didjwk.NewResolver(), }, } - err := sv.VerifySignature(*cred, nil) + err = sv.VerifySignature(*cred, nil) - assert.EqualError(t, err, "presentation(s) or credential(s) verification failed: unable to validate JWT signature: could not verify message using any of the signatures or keys") + assert.EqualError(t, err, "presentation(s) or credential(s) verification failed: unable to validate JWT signature: token signing algorithm is not supported: none") }) }) @@ -233,7 +241,7 @@ func TestSignatureVerifier_VerifySignature(t *testing.T) { err := sv.VerifySignature(vc2, nil) - assert.ErrorContains(t, err, "failed to verify signature") + assert.ErrorContains(t, err, "invalid ECDSA signature") }) t.Run("error - wrong hashed proof", func(t *testing.T) { @@ -248,7 +256,7 @@ func TestSignatureVerifier_VerifySignature(t *testing.T) { err := sv.VerifySignature(vc2, nil) - assert.ErrorContains(t, err, "failed to verify signature") + assert.ErrorContains(t, err, "invalid ECDSA signature") }) t.Run("error - no proof", func(t *testing.T) { @@ -321,7 +329,7 @@ func buildX509Credential(chain *cert.Chain, signingCert *x509.Certificate, rootC claims["vc"] = *credential - token, err := nutsCrypto.SignJWT(audit.TestContext(), signingKey, jwa.PS512, claims, headers) + token, err := nutsCrypto.SignJWT(audit.TestContext(), signingKey, jwa.PS512(), claims, headers) if err != nil { return nil, err } diff --git a/vcr/verifier/verifier_test.go b/vcr/verifier/verifier_test.go index 3466d519e4..bef639b765 100644 --- a/vcr/verifier/verifier_test.go +++ b/vcr/verifier/verifier_test.go @@ -35,8 +35,8 @@ import ( "github.com/nuts-foundation/nuts-node/storage/orm" "github.com/nuts-foundation/nuts-node/test/pki" - "github.com/lestrrat-go/jwx/v2/jwk" - "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/lestrrat-go/jwx/v3/jwk" + "github.com/lestrrat-go/jwx/v3/jwt" ssi "github.com/nuts-foundation/go-did" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/go-did/vc" @@ -452,7 +452,7 @@ func Test_verifier_CheckAndStoreRevocation(t *testing.T) { otherKey, _ := spi.GenerateKeyPair() sut.keyResolver.EXPECT().ResolveKeyByID(revocation.Proof.VerificationMethod.String(), &metadata, resolver.NutsSigningKeyType).Return(otherKey, nil) err := sut.verifier.RegisterRevocation(revocation) - assert.EqualError(t, err, "unable to verify revocation signature: invalid proof signature: failed to verify signature using ecdsa") + assert.EqualError(t, err, "unable to verify revocation signature: invalid proof signature: invalid ECDSA signature") }) } @@ -469,7 +469,7 @@ func TestVerifier_VerifyVP(t *testing.T) { }`)) require.NoError(t, err) var publicKey crypto.PublicKey - require.NoError(t, key.Raw(&publicKey)) + require.NoError(t, jwk.Export(key, &publicKey)) const rawVP = `eyJhbGciOiJFUzI1NiIsImtpZCI6ImRpZDpudXRzOkd2a3p4c2V6SHZFYzhuR2hnejZYbzNqYnFrSHdzd0xtV3czQ1l0Q203aEFXI2FiYy1tZXRob2QtMSIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2OTc2OTY3NDEsImlzcyI6ImRpZDpudXRzOkd2a3p4c2V6SHZFYzhuR2hnejZYbzNqYnFrSHdzd0xtV3czQ1l0Q203aEFXIiwibmJmIjoxNjk3NjEwMzQxLCJzdWIiOiJkaWQ6bnV0czpHdmt6eHNlekh2RWM4bkdoZ3o2WG8zamJxa0h3c3dMbVd3M0NZdENtN2hBVyIsInZwIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sInR5cGUiOiJWZXJpZmlhYmxlUHJlc2VudGF0aW9uIiwidmVyaWZpYWJsZUNyZWRlbnRpYWwiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJodHRwczovL251dHMubmwvY3JlZGVudGlhbHMvdjEiLCJodHRwczovL3czYy1jY2cuZ2l0aHViLmlvL2xkcy1qd3MyMDIwL2NvbnRleHRzL2xkcy1qd3MyMDIwLXYxLmpzb24iXSwiY3JlZGVudGlhbFN1YmplY3QiOnsiY29tcGFueSI6eyJjaXR5IjoiSGVuZ2VsbyIsIm5hbWUiOiJEZSBiZXN0ZSB6b3JnIn0sImlkIjoiZGlkOm51dHM6R3ZrenhzZXpIdkVjOG5HaGd6NlhvM2picWtId3N3TG1XdzNDWXRDbTdoQVcifSwiaWQiOiJkaWQ6bnV0czo0dHpNYVdmcGl6VktlQThmc2NDM0pUZFdCYzNhc1VXV01qNWhVRkhkV1gzSCNmNDNiZWY0Zi0xYTc5LTQzNjQtOTJmMy0zZmM3NDNmYTlmMTkiLCJpc3N1YW5jZURhdGUiOiIyMDIxLTEyLTI0VDEzOjIxOjI5LjA4NzIwNSswMTowMCIsImlzc3VlciI6ImRpZDpudXRzOjR0ek1hV2ZwaXpWS2VBOGZzY0MzSlRkV0JjM2FzVVdXTWo1aFVGSGRXWDNIIiwicHJvb2YiOnsiY3JlYXRlZCI6IjIwMjEtMTItMjRUMTM6MjE6MjkuMDg3MjA1KzAxOjAwIiwiandzIjoiZXlKaGJHY2lPaUpGVXpJMU5pSXNJbUkyTkNJNlptRnNjMlVzSW1OeWFYUWlPbHNpWWpZMElsMTkuLmhQTTJHTGMxSzlkMkQ4U2J2ZTAwNHg5U3VtakxxYVhUaldoVWh2cVdSd3hmUldsd2ZwNWdIRFVZdVJvRWpoQ1hmTHQtX3Uta25DaFZtSzk4ME4zTEJ3IiwicHJvb2ZQdXJwb3NlIjoiTnV0c1NpZ25pbmdLZXlUeXBlIiwidHlwZSI6Ikpzb25XZWJTaWduYXR1cmUyMDIwIiwidmVyaWZpY2F0aW9uTWV0aG9kIjoiZGlkOm51dHM6R3ZrenhzZXpIdkVjOG5HaGd6NlhvM2picWtId3N3TG1XdzNDWXRDbTdoQVcjYWJjLW1ldGhvZC0xIn0sInR5cGUiOlsiQ29tcGFueUNyZWRlbnRpYWwiLCJWZXJpZmlhYmxlQ3JlZGVudGlhbCJdfX19.v3beJvGa3HeImU3VLvsrZjnHs0krKPaCdTEh-qHS7j26LIQYcMHhrLkIexrpPO5z0TKSDnKq5Jl10SWaJpLRIA` @@ -510,7 +510,8 @@ func TestVerifier_VerifyVP(t *testing.T) { } t.Run("ok - credential has no own proof", func(t *testing.T) { vp, key := test.CreateJWTPresentation(t, subjectDID, func(token jwt.Token) { - vpRaw, _ := token.Get("vp") + var vpRaw interface{} + _ = token.Get("vp", &vpRaw) castVP := vpRaw.(vc.VerifiablePresentation) castVP.Holder, _ = ssi.ParseURI(subjectDID.String()) _ = token.Set("vp", castVP) @@ -547,7 +548,7 @@ func TestVerifier_VerifyVP(t *testing.T) { validAt := time.Date(2023, 10, 21, 12, 0, 0, 0, time.UTC) vcs, err := ctx.verifier.VerifyVP(*presentation, false, false, &validAt) - assert.EqualError(t, err, "presentation(s) or credential(s) verification failed: unable to validate JWT signature: \"exp\" not satisfied") + assert.EqualError(t, err, "presentation(s) or credential(s) verification failed: unable to validate JWT signature: jwt.ParseString: failed to parse string: jwt.Validate: validation failed: \"exp\" not satisfied: token is expired") assert.Empty(t, vcs) }) t.Run("VP signer != VC credentialSubject.id", func(t *testing.T) { @@ -710,7 +711,7 @@ func TestVerifier_VerifyVP(t *testing.T) { vcs, err := ctx.verifier.VerifyVP(vp, false, false, validAt) - assert.EqualError(t, err, "presentation(s) or credential(s) verification failed: invalid signature: invalid proof signature: failed to verify signature using ecdsa") + assert.EqualError(t, err, "presentation(s) or credential(s) verification failed: invalid signature: invalid proof signature: invalid ECDSA signature") assert.Empty(t, vcs) }) t.Run("error - signing key unknown", func(t *testing.T) { diff --git a/vdr/didjwk/resolver.go b/vdr/didjwk/resolver.go index dae339209d..7989067693 100644 --- a/vdr/didjwk/resolver.go +++ b/vdr/didjwk/resolver.go @@ -27,7 +27,7 @@ import ( godid "github.com/nuts-foundation/go-did" "github.com/nuts-foundation/go-did/did" - "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v3/jwk" ) // MethodName is the name of this DID method. @@ -117,7 +117,7 @@ func rawPrivateKeyOf(key jwk.Key) (any, error) { // value is available. If the jwx/jwk library offers a way in the future to specifically get a private key from // a JWK this will be much simpler. var rawUnspecifiedKey any - if err := key.Raw(&rawUnspecifiedKey); err != nil { + if err := jwk.Export(key, &rawUnspecifiedKey); err != nil { return nil, fmt.Errorf("failed to get raw key: %w", err) } @@ -132,7 +132,7 @@ func rawPrivateKeyOf(key jwk.Key) (any, error) { // Get the raw public key, which is a golang crypto primitive, or nil. This will be compared next to the // rawUnspecifiedKey (unspecified as public or private) key to determine whether a private key can be returned. var rawPublicKey any - if err := publicKey.Raw(&rawPublicKey); err != nil { + if err := jwk.Export(publicKey, &rawPublicKey); err != nil { return nil, fmt.Errorf("failed to get raw public key: %w", err) } diff --git a/vdr/didjwk/resolver_test.go b/vdr/didjwk/resolver_test.go index 0be0e9aee1..f6186ff13c 100644 --- a/vdr/didjwk/resolver_test.go +++ b/vdr/didjwk/resolver_test.go @@ -87,7 +87,7 @@ func TestResolver_Resolve(t *testing.T) { t.Run("Invalid base64 data fails", failure(b64(canonicalJWK)+":invalid-base64-data", "illegal base64 data at input")) // Ensure a DID with invalid JSON fails - t.Run("base64 encoded non-JSON fails", failure(b64("__NOT_JSON__"), "failed to unmarshal JSON")) + t.Run("base64 encoded non-JSON fails", failure(b64("__NOT_JSON__"), "failed to unmarshal data")) // Ensure valid JSON as an invalid JWK fails t.Run("base64+JSON encoded non-JWK fails", failure(b64(validJSONInvalidJWK), "invalid key type from JSON")) diff --git a/vdr/didkey/resolver.go b/vdr/didkey/resolver.go index 1d84a5809c..e03235a105 100644 --- a/vdr/didkey/resolver.go +++ b/vdr/didkey/resolver.go @@ -21,6 +21,7 @@ package didkey import ( "bytes" "crypto" + "crypto/ecdh" "crypto/ecdsa" "crypto/ed25519" "crypto/elliptic" @@ -28,7 +29,6 @@ import ( "encoding/binary" "errors" "fmt" - "github.com/lestrrat-go/jwx/v2/x25519" "github.com/mr-tron/base58" "github.com/multiformats/go-multicodec" ssi "github.com/nuts-foundation/go-did" @@ -81,7 +81,9 @@ func (r Resolver) Resolve(id did.DID, _ *resolver.ResolveMetadata) (*did.Documen if keyLength != 32 { return nil, nil, errInvalidPublicKeyLength } - key = x25519.PublicKey(mcBytes) + if key, err = ecdh.X25519().NewPublicKey(mcBytes); err != nil { + return nil, nil, fmt.Errorf("did:key: invalid X25519 public key: %w", err) + } case multicodec.Ed25519Pub: if keyLength != 32 { return nil, nil, errInvalidPublicKeyLength diff --git a/vdr/didnuts/ambassador.go b/vdr/didnuts/ambassador.go index 837962b048..c25a1550ee 100644 --- a/vdr/didnuts/ambassador.go +++ b/vdr/didnuts/ambassador.go @@ -26,7 +26,7 @@ import ( "encoding/json" "errors" "fmt" - "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v3/jwk" "github.com/nats-io/nats.go" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/go-stoabs" @@ -210,7 +210,7 @@ func (n *ambassador) handleCreateDIDDocument(transaction dag.Transaction, propos } var rawKey crypto.PublicKey - err = transaction.SigningKey().Raw(&rawKey) + err = jwk.Export(transaction.SigningKey(), &rawKey) if err != nil { return err } @@ -283,7 +283,7 @@ func (n *ambassador) handleUpdateDIDDocument(transaction dag.Transaction, propos return fmt.Errorf("unable to resolve signingkey: %w", err) } - signingKey, err := jwk.FromRaw(pKey) + signingKey, err := jwk.Import(pKey) if err != nil { return fmt.Errorf("could not parse public key into jwk: %w", err) } diff --git a/vdr/didnuts/ambassador_test.go b/vdr/didnuts/ambassador_test.go index e762aa7987..0532e1ad98 100644 --- a/vdr/didnuts/ambassador_test.go +++ b/vdr/didnuts/ambassador_test.go @@ -30,7 +30,7 @@ import ( "time" "github.com/google/uuid" - "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v3/jwk" "github.com/nats-io/nats.go" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/nuts-node/audit" @@ -357,7 +357,7 @@ func TestAmbassador_handleCreateDIDDocument(t *testing.T) { t.Run("create nok - fails when DID does not matches signing key", func(t *testing.T) { ctx := newMockContext(t) pair, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - signingKey, _ := jwk.FromRaw(pair.PublicKey) + signingKey, _ := jwk.Import(pair.PublicKey) _ = signingKey.Set(jwk.KeyIDKey, "kid123") tx := newTX() tx.signingKeyID = "" @@ -417,7 +417,7 @@ func TestAmbassador_handleUpdateDIDDocument(t *testing.T) { } var pKey crypto.PublicKey - _ = signingKey.Raw(&pKey) + _ = jwk.Export(signingKey, &pKey) ctx.didStore.EXPECT().Resolve(didDocument.ID, &resolver.ResolveMetadata{AllowDeactivated: true}).Return(&storedDocument, currentMetadata, nil) ctx.keyResolver.EXPECT().ResolvePublicKey(storedDocument.CapabilityInvocation[0].ID.String(), gomock.Any()).Return(pKey, nil) @@ -454,7 +454,7 @@ func TestAmbassador_handleUpdateDIDDocument(t *testing.T) { } var pKey crypto.PublicKey - _ = signingKey.Raw(&pKey) + _ = jwk.Export(signingKey, &pKey) ctx.didStore.EXPECT().Resolve(currentDoc.ID, &resolver.ResolveMetadata{AllowDeactivated: true, SourceTransaction: &prev}).Return(¤tDoc, currentMetadata, nil) ctx.keyResolver.EXPECT().ResolvePublicKey(currentDoc.CapabilityInvocation[0].ID.String(), gomock.Any()).Return(pKey, nil) @@ -489,7 +489,7 @@ func TestAmbassador_handleUpdateDIDDocument(t *testing.T) { } var pKey crypto.PublicKey - _ = signingKey.Raw(&pKey) + _ = jwk.Export(signingKey, &pKey) gomock.InOrder( ctx.didStore.EXPECT().Resolve(currentDoc.ID, gomock.Any()).Return(nil, nil, resolver.ErrNotFound), @@ -513,7 +513,7 @@ func TestAmbassador_handleUpdateDIDDocument(t *testing.T) { didDocumentController, controllerSigningKey := newDidDoc(t) var pKey crypto.PublicKey - _ = controllerSigningKey.Raw(&pKey) + _ = jwk.Export(controllerSigningKey, &pKey) // set the didDocument`s controller to the controller didDocument.Controller = []did.DID{didDocumentController.ID} @@ -570,7 +570,7 @@ func TestAmbassador_handleUpdateDIDDocument(t *testing.T) { // We still use the document signing key, not the one from the controller var pKey crypto.PublicKey - _ = documentSigningKey.Raw(&pKey) + _ = jwk.Export(documentSigningKey, &pKey) // set the didDocument`s controller to the controller didDocument.Controller = []did.DID{didDocumentController.ID} @@ -644,7 +644,7 @@ func Test_handleUpdateDIDDocument(t *testing.T) { func Test_checkTransactionIntegrity(t *testing.T) { signingTime := time.Now() pair, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - signingKey, _ := jwk.FromRaw(pair.PublicKey) + signingKey, _ := jwk.Import(pair.PublicKey) _ = signingKey.Set(jwk.KeyIDKey, "kid123") payloadHash := hash.SHA256Sum([]byte("payload")) ref := hash.SHA256Sum([]byte("ref")) @@ -737,7 +737,7 @@ func newDidDoc(t *testing.T) (did.Document, jwk.Key) { require.NoError(t, err) publicKey, err := didDocument.VerificationMethod[0].PublicKey() require.NoError(t, err) - signingKey, _ := jwk.FromRaw(publicKey) + signingKey, _ := jwk.Import(publicKey) serviceID := did.MustParseDIDURL(didDocument.ID.String()) serviceID.Fragment = "1234" didDocument.Service = []did.Service{ diff --git a/vdr/didnuts/manager.go b/vdr/didnuts/manager.go index a2341a4d91..77824252c1 100644 --- a/vdr/didnuts/manager.go +++ b/vdr/didnuts/manager.go @@ -27,7 +27,7 @@ import ( "github.com/nuts-foundation/nuts-node/storage" "time" - "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v3/jwk" ssi "github.com/nuts-foundation/go-did" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/nuts-node/core" @@ -106,7 +106,7 @@ func getKIDName(pKey crypto.PublicKey, idFunc func(key jwk.Key) (string, error)) // -------------------- // generate idString - jwKey, err := jwk.FromRaw(pKey) + jwKey, err := jwk.Import(pKey) if err != nil { return "", fmt.Errorf("could not generate kid: %w", err) } @@ -126,7 +126,7 @@ func getKIDName(pKey crypto.PublicKey, idFunc func(key jwk.Key) (string, error)) kid := &did.DIDURL{} kid.Method = MethodName kid.ID = idString - kid.Fragment = jwKey.KeyID() + kid.Fragment, _ = jwKey.KeyID() return kid.String(), nil } diff --git a/vdr/didnuts/manager_test.go b/vdr/didnuts/manager_test.go index bd17138ed0..948eb8b6f9 100644 --- a/vdr/didnuts/manager_test.go +++ b/vdr/didnuts/manager_test.go @@ -31,7 +31,7 @@ import ( "testing" "github.com/google/uuid" - "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v3/jwk" ssi "github.com/nuts-foundation/go-did" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/nuts-node/audit" @@ -189,8 +189,9 @@ func TestManager_GenerateDocument(t *testing.T) { nutsThumbprint, err := nutsCrypto.Thumbprint(asJWK) require.NoError(t, err) _ = jwk.AssignKeyID(asJWK, jwk.WithThumbprintHash(crypto.SHA256)) + asJWKKeyID, _ := asJWK.KeyID() assert.Equal(t, fmt.Sprintf("did:nuts:%s", nutsThumbprint), doc.DID.ID) - assert.Equal(t, fmt.Sprintf("did:nuts:%s#%s", nutsThumbprint, asJWK.KeyID()), verificationMethod.ID.String()) + assert.Equal(t, fmt.Sprintf("did:nuts:%s#%s", nutsThumbprint, asJWKKeyID), verificationMethod.ID.String()) t.Run("additional verification method", func(t *testing.T) { asDID := did.MustParseDID(doc.DID.ID) @@ -202,7 +203,8 @@ func TestManager_GenerateDocument(t *testing.T) { asJWK, err := verificationMethod.JWK() require.NoError(t, err) _ = jwk.AssignKeyID(asJWK, jwk.WithThumbprintHash(crypto.SHA256)) - assert.Equal(t, fmt.Sprintf("%s#%s", doc.DID.ID, asJWK.KeyID()), verificationMethod.ID.String()) + asJWKKeyID, _ := asJWK.KeyID() + assert.Equal(t, fmt.Sprintf("%s#%s", doc.DID.ID, asJWKKeyID), verificationMethod.ID.String()) }) }) } @@ -250,7 +252,7 @@ func Test_DIDKidNamingFunc(t *testing.T) { t.Run("nok - wrong key type", func(t *testing.T) { keyID, err := DIDKIDNamingFunc(unknownPublicKey{}) - assert.EqualError(t, err, "could not generate kid: invalid key type 'didnuts.unknownPublicKey' for jwk.New") + assert.EqualError(t, err, "could not generate kid: jwk.Import: failed to convert didnuts.unknownPublicKey to jwk.Key: no converters were able to convert") assert.Empty(t, keyID) }) } @@ -278,7 +280,7 @@ func jwkToPublicKey(t *testing.T, jwkStr string) (crypto.PublicKey, error) { require.NoError(t, err) key, _ := keySet.Key(0) var rawKey crypto.PublicKey - if err = key.Raw(&rawKey); err != nil { + if err = jwk.Export(key, &rawKey); err != nil { return nil, err } return rawKey, nil @@ -308,8 +310,9 @@ func TestManager_NewDocument(t *testing.T) { nutsThumbprint, err := nutsCrypto.Thumbprint(asJWK) require.NoError(t, err) _ = jwk.AssignKeyID(asJWK, jwk.WithThumbprintHash(crypto.SHA256)) + asJWKKeyID, _ := asJWK.KeyID() assert.Equal(t, fmt.Sprintf("did:nuts:%s", nutsThumbprint), doc.DID.ID) - assert.Equal(t, fmt.Sprintf("did:nuts:%s#%s", nutsThumbprint, asJWK.KeyID()), verificationMethod.ID.String()) + assert.Equal(t, fmt.Sprintf("did:nuts:%s#%s", nutsThumbprint, asJWKKeyID), verificationMethod.ID.String()) t.Run("additional verification method", func(t *testing.T) { asDID := did.MustParseDID(doc.DID.ID) @@ -321,7 +324,8 @@ func TestManager_NewDocument(t *testing.T) { asJWK, err := verificationMethod.JWK() require.NoError(t, err) _ = jwk.AssignKeyID(asJWK, jwk.WithThumbprintHash(crypto.SHA256)) - assert.Equal(t, fmt.Sprintf("%s#%s", doc.DID.ID, asJWK.KeyID()), verificationMethod.ID.String()) + asJWKKeyID, _ := asJWK.KeyID() + assert.Equal(t, fmt.Sprintf("%s#%s", doc.DID.ID, asJWKKeyID), verificationMethod.ID.String()) }) }) } diff --git a/vdr/didnuts/validators.go b/vdr/didnuts/validators.go index ed10fddd2b..d0bfbac6ff 100644 --- a/vdr/didnuts/validators.go +++ b/vdr/didnuts/validators.go @@ -22,7 +22,7 @@ import ( "encoding/json" "errors" "fmt" - "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v3/jwk" ssi "github.com/nuts-foundation/go-did" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/nuts-node/network/transport" @@ -79,7 +79,7 @@ func (v verificationMethodValidator) verifyThumbprint(method *did.VerificationMe return fmt.Errorf("unable to get JWK: %w", err) } _ = jwk.AssignKeyID(keyAsJWK) - if keyAsJWK.KeyID() != method.ID.Fragment { + if keyID, _ := keyAsJWK.KeyID(); keyID != method.ID.Fragment { return errors.New("key thumbprint does not match ID") } return nil diff --git a/vdr/didx509/resolver_test.go b/vdr/didx509/resolver_test.go index a9c56b3c50..4f8069e71e 100644 --- a/vdr/didx509/resolver_test.go +++ b/vdr/didx509/resolver_test.go @@ -23,7 +23,7 @@ import ( "crypto/x509" "encoding/base64" "fmt" - "github.com/lestrrat-go/jwx/v2/cert" + "github.com/lestrrat-go/jwx/v3/cert" "github.com/minio/sha256-simd" "github.com/nuts-foundation/go-did/did" testpki "github.com/nuts-foundation/nuts-node/test/pki" diff --git a/vdr/didx509/x509_utils.go b/vdr/didx509/x509_utils.go index b4be0cacd6..300ecdae39 100644 --- a/vdr/didx509/x509_utils.go +++ b/vdr/didx509/x509_utils.go @@ -29,7 +29,7 @@ import ( "encoding/base64" "errors" "fmt" - "github.com/lestrrat-go/jwx/v2/cert" + "github.com/lestrrat-go/jwx/v3/cert" "strings" ) diff --git a/vdr/didx509/x509_utils_test.go b/vdr/didx509/x509_utils_test.go index 41f98cfb19..64f70b87b5 100644 --- a/vdr/didx509/x509_utils_test.go +++ b/vdr/didx509/x509_utils_test.go @@ -26,7 +26,7 @@ import ( "encoding/base64" "errors" "fmt" - "github.com/lestrrat-go/jwx/v2/cert" + "github.com/lestrrat-go/jwx/v3/cert" "github.com/nuts-foundation/nuts-node/test/pki" "github.com/stretchr/testify/require" "slices" @@ -176,16 +176,6 @@ func TestFindCertificateByHash(t *testing.T) { // TestParseChain tests the parseChain function with cases that contain valid and invalid PEM encoded certificates. func TestParseChain(t *testing.T) { - newChain := func(pems [][]byte) *cert.Chain { - chain := cert.Chain{} - for _, pemCert := range pems { - err := chain.Add(pemCert) - if err != nil { - panic(err) - } - } - return &chain - } certs, _, _ := pki.BuildCertChain([]string{"123"}, "", nil) invalidCert := `Y29ycnVwdCBjZXJ0aWZpY2F0ZQo=` @@ -206,16 +196,6 @@ func TestParseChain(t *testing.T) { chain: pki.CertsToChain(certs), want: certs[:], // not critical for testing wantError: nil, - }, { - name: "invalid cert", - chain: newChain([][]byte{[]byte(invalidCert)}), - want: nil, - wantError: errors.New("x509: malformed certificate"), - }, { - name: "invalid base64", - chain: newChain([][]byte{[]byte(invalidBase64)}), - want: nil, - wantError: errors.New("illegal base64 data at input byte 5"), }, } @@ -233,6 +213,20 @@ func TestParseChain(t *testing.T) { } }) } + + // In jwx v3, cert.Chain.Add parses and validates each certificate eagerly, so + // malformed certificates are rejected at insertion time and can never reach + // parseChain. Assert that the malformed-cert handling still happens, now at Add. + t.Run("invalid cert", func(t *testing.T) { + chain := cert.Chain{} + err := chain.Add([]byte(invalidCert)) + require.ErrorContains(t, err, "x509: malformed certificate") + }) + t.Run("invalid base64", func(t *testing.T) { + chain := cert.Chain{} + err := chain.Add([]byte(invalidBase64)) + require.ErrorContains(t, err, "illegal base64 data at input byte 5") + }) } func leafCertFromCerts(certs []*x509.Certificate) *x509.Certificate { diff --git a/vdr/resolver/did.go b/vdr/resolver/did.go index 052bf8dd9f..09b7577e54 100644 --- a/vdr/resolver/did.go +++ b/vdr/resolver/did.go @@ -20,7 +20,7 @@ package resolver import ( "errors" - "github.com/lestrrat-go/jwx/v2/cert" + "github.com/lestrrat-go/jwx/v3/cert" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/nuts-node/crypto/hash" "sync" diff --git a/vdr/resolver/did_test.go b/vdr/resolver/did_test.go index 1db1c60254..bbad2a808a 100644 --- a/vdr/resolver/did_test.go +++ b/vdr/resolver/did_test.go @@ -23,7 +23,7 @@ import ( "crypto/elliptic" "crypto/rand" "errors" - "github.com/lestrrat-go/jwx/v2/cert" + "github.com/lestrrat-go/jwx/v3/cert" ssi "github.com/nuts-foundation/go-did" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/nuts-node/crypto/hash" diff --git a/vdr/vdr_test.go b/vdr/vdr_test.go index ce6c593695..5207d19711 100644 --- a/vdr/vdr_test.go +++ b/vdr/vdr_test.go @@ -25,7 +25,7 @@ import ( "crypto/rand" "encoding/base64" "encoding/json" - "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v3/jwk" ssi "github.com/nuts-foundation/go-did" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/nuts-node/audit" @@ -265,7 +265,7 @@ func TestVDR_Configure(t *testing.T) { }) t.Run("it can resolve using did:jwk", func(t *testing.T) { privateKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - expectedJWK, err := jwk.FromRaw(privateKey.Public()) + expectedJWK, err := jwk.Import(privateKey.Public()) require.NoError(t, err) jwkBytes, _ := json.Marshal(expectedJWK)