From 022adb4d7fe25f452dfeb5fd1a58ba9017ba3725 Mon Sep 17 00:00:00 2001 From: reinkrul Date: Tue, 14 Jul 2026 13:18:18 +0200 Subject: [PATCH 1/2] chore: migrate from lestrrat-go/jwx v2 (EOL) to v3 (#4391) * fix: unwrap error chain when resolving HTTP status code GetHTTPStatusCode only did a direct type assertion for HTTPStatusCodeError, so any fmt.Errorf("...: %w", err) wrap between an error's origin and the Echo error handler silently discarded a deliberately-set status code, falling back to 500. This affected RequestServiceAccessToken when the remote presentation definition endpoint returns an OAuth2 error (e.g. invalid_scope), among other paths, per #2943. * chore: migrate from lestrrat-go/jwx v2 (EOL) to v3 jwx v2 is deprecated (EOL as of v2.1.7); migrate all direct usage to v3. - Rewrite imports v2 -> v3 across the codebase. - jwa algorithm constants -> function calls (jwa.ES256 -> jwa.ES256()). - Adapt Get()/accessor call sites to v3 signatures ((T, bool) returns). - jwk.FromRaw -> jwk.Import, (jwk.Key).Raw -> jwk.Export. - Replace the removed raw jws.NewVerifier with jwsbb.Verify for detached signature verification (crypto, vcr JSON-LD proof). - didkey resolver: use crypto/ecdh for X25519 (jwx/x25519 removed in v3). - Faithfully replace token.AsMap/PrivateClaims with JSON-based extraction so null-valued claims are still tolerated (v3 per-claim Get errors on null). - azure keyvault: validate key type before jwk.Export for a clear error. - Point go-did at its jwx-v3 branch (pending a tagged go-did release). Assisted by AI * refactor(jwx): centralize JWT claims-to-map into jwx.AsMap; check errors Addresses review feedback on the jwx v3 migration: - Add crypto/jwx.AsMap(joseObject) helper that converts a jwt.Token's claims to a map via JSON (preserving null-valued claims, unlike a per-field Get loop which errors on null). Replaces the duplicated marshal/unmarshal blocks in auth/client/iam, auth/api/iam/jar, auth/services/oauth, and the vcr test helper, and the equivalent loop in spi.FromJWK. The helper checks both the marshal and unmarshal errors, so those are no longer ignored. - Keep the per-field Get loop for JOSE *headers* (DecryptJWE, ExtractProtectedHeaders): a JSON round-trip flattens rich header values (e.g. the x5c certificate chain) and breaks callers that rely on their concrete Go types. - Remaining discarded returns are single-value accessors that returned no error/bool in v2 (e.g. token.Issuer()), so ignoring the new bool is faithful. Assisted by AI * chore: pin go-did to tagged release v0.22.0 Replaces the interim jwx-v3-migration branch pseudo-version with the tagged go-did v0.22.0 release (jwx v3). Clears the merge blocker for the jwx v3 migration. Assisted by AI * fix(iam): handle []interface{} array claims in oauthParameters.get The jwx v3 migration extracts JAR request-object claims via jwx.AsMap (json.Marshal + Unmarshal into a map), which decodes JSON arrays as []interface{} rather than the []string that jwx v2's AsMap produced. oauthParameters.get only matched []string, so a single-value "aud" claim resolved to "" and audience validation failed (surfaced by the e2e OAuth flow: "invalid audience, expected: ..., was: "). Handle []interface{} single-element arrays too, fulfilling get's documented "array values with len == 1 are treated as single string values" contract regardless of whether the value came from query params ([]string) or JSON claims ([]interface{}). Add a unit test covering the accessor. Assisted by AI * refactor(jwx): use jws.VerifierFor over the low-level jwsbb.Verify For the raw/detached signature verification (ParseJWS and the JSON-LD proof verifier), use the jws-level jws.VerifierFor(alg) instead of reaching into the low-level jwsbb.Verify building block. jws.VerifierFor is the intended v3 replacement for the v2 jws.NewVerifier(alg) pattern (which no longer registers standard algorithms in v3). For standard algorithms it delegates to jwsbb.Verify anyway, so the result is identical, but it stays at the public jws API level and honours any verifier registered via jws.RegisterVerifier (jwsbb.Verify bypasses the registry by design). Assisted by AI * fix(jwx): jwa.ES256K() function call under jwx_es256k build tag The es256k registration file is only compiled with -tags jwx_es256k, so the jwa constant->function change was missed during the v2->v3 migration. Assisted by AI * fix(jwx): tolerate null-valued JOSE header members; restore ES256K test Two follow-ups to the jwx v2->v3 migration: - Null-valued protected header members were rejected. The header extraction loops (DecryptJWE, ExtractProtectedHeaders) use per-field Get to preserve rich types like the x5c certificate chain, but v3's Get errors on a null value, so a JWS/JWE with a null-valued custom header was rejected where v2's Headers.AsMap returned nil. Add jwx.HeadersAsMap, which keeps concrete Go types and stores a null member as nil (the only failure mode of Get on an existing key into interface{}). Use it in both loops. - Move and fix the previously-uncompilable ES256K test. It lived in package jwx but called crypto.ParseJWT (an import cycle), passed the wrong argument count, and signed with a P-256 key for a secp256k1 algorithm. It never compiled because CI only builds -tags jwx_es256k, never tests it. Moved to package crypto and signed with a real secp256k1 key. Assisted by AI * refactor(jwx): rename AsMap to ClaimsAsMap, type it as jwt.Token AsMap was misleadingly generic: it took `any` but is really about a JWT token's claims. Rename to ClaimsAsMap and type the parameter as jwt.Token for compile-time safety and clearer intent. The one non-token caller (spi.FromJWK, a jwk.Key) is not "claims", so it now does its own json.Marshal/Unmarshal inline. A jwk.Key has no null-valued members, so it does not need the null-tolerant helper. Assisted by AI * refactor(jwx): make headersAsMap an unexported crypto-package helper Its only callers are DecryptJWE and ExtractProtectedHeaders in the crypto package, so move it out of the exported crypto/jwx API and unexport it. Drop its unit test. Assisted by AI * docs(jwx): drop headers caveat from ClaimsAsMap doc Assisted by AI * JWK missing checks (cherry picked from commit 9bf07c1b36b9bf6fad671a4227d2bb06fbaece35) --- auth/api/iam/api.go | 12 +- auth/api/iam/api_test.go | 8 +- auth/api/iam/dpop.go | 9 +- auth/api/iam/dpop_test.go | 14 +- auth/api/iam/jar.go | 9 +- auth/api/iam/jar_test.go | 14 +- auth/api/iam/metadata.go | 10 +- auth/api/iam/openid4vci.go | 2 +- auth/api/iam/openid4vp.go | 7 +- auth/api/iam/openid4vp_test.go | 2 +- auth/api/iam/params.go | 7 + auth/api/iam/params_test.go | 44 ++++++ auth/api/iam/s2s_vptoken.go | 3 +- auth/api/iam/s2s_vptoken_test.go | 4 +- auth/api/iam/user_test.go | 6 +- auth/api/iam/validation.go | 2 +- auth/client/iam/client.go | 12 +- auth/client/iam/client_test.go | 11 +- auth/oauth/types.go | 2 +- auth/oauth/types_test.go | 2 +- auth/services/irma/validator.go | 2 +- auth/services/oauth/authz_server.go | 72 ++++++---- auth/services/oauth/authz_server_test.go | 46 +++---- auth/services/oauth/relying_party.go | 2 +- core/echo_errors.go | 2 +- crypto/api/v1/api.go | 2 +- crypto/dpop/dpop.go | 63 +++++---- crypto/dpop/dpop_test.go | 57 ++++---- crypto/dpop_test.go | 15 +- crypto/jwx.go | 128 ++++++++++-------- crypto/jwx/algorithm.go | 12 +- crypto/jwx/claims.go | 43 ++++++ crypto/jwx/jwx_es256k.go | 4 +- crypto/{jwx => }/jwx_es256k_test.go | 35 +++-- crypto/jwx_test.go | 109 ++++++++------- crypto/memory.go | 10 +- crypto/memory_test.go | 6 +- crypto/storage/azure/keyvault.go | 10 +- crypto/storage/azure/keyvault_test.go | 19 ++- crypto/storage/spi/interface.go | 8 +- crypto/storage/spi/interface_test.go | 6 +- discovery/client_test.go | 2 +- discovery/module.go | 14 +- discovery/module_test.go | 2 +- discovery/store.go | 3 +- discovery/store_test.go | 2 +- discovery/test.go | 15 +- go.mod | 34 ++--- go.sum | 76 ++++++----- http/engine_test.go | 10 +- http/tokenV2/authorized_keys.go | 4 +- http/tokenV2/middleware.go | 64 +++++---- http/tokenV2/middleware_test.go | 101 +++++--------- http/user/session.go | 2 +- http/user/session_test.go | 4 +- network/dag/parser.go | 40 +++--- network/dag/parser_test.go | 75 +++++----- network/dag/signing.go | 6 +- network/dag/signing_test.go | 2 +- network/dag/test.go | 4 +- network/dag/transaction.go | 6 +- network/dag/verifier.go | 13 +- network/dag/verifier_test.go | 8 +- .../v2/transactionlist_handler_test.go | 2 +- pki/denylist.go | 16 +-- pki/denylist_test.go | 12 +- test/pki/test.go | 2 +- vcr/credential/resolver_test.go | 12 +- vcr/credential/util.go | 7 +- vcr/credential/util_test.go | 2 +- vcr/credential/validator.go | 2 +- vcr/credential/validator_test.go | 17 ++- vcr/holder/presenter.go | 4 +- vcr/holder/presenter_test.go | 19 ++- vcr/issuer/issuer_test.go | 12 +- vcr/issuer/openid.go | 17 +-- vcr/issuer/openid_test.go | 2 +- vcr/pe/presentation_definition.go | 7 +- vcr/pe/presentation_definition_test.go | 14 +- vcr/pe/util.go | 8 +- vcr/pe/util_test.go | 4 +- vcr/signature/json_web_signature.go | 2 +- vcr/signature/json_web_signature_test.go | 15 +- vcr/signature/proof/jsonld.go | 9 +- vcr/signature/proof/jsonld_test.go | 2 +- vcr/store_test.go | 3 +- vcr/test/credentials.go | 12 +- vcr/test/test.go | 9 +- vcr/verifier/signature_verifier.go | 2 +- vcr/verifier/signature_verifier_test.go | 34 +++-- vcr/verifier/verifier_test.go | 15 +- vdr/didjwk/resolver.go | 6 +- vdr/didjwk/resolver_test.go | 2 +- vdr/didkey/resolver.go | 6 +- vdr/didnuts/ambassador.go | 6 +- vdr/didnuts/ambassador_test.go | 18 +-- vdr/didnuts/manager.go | 6 +- vdr/didnuts/manager_test.go | 18 ++- vdr/didnuts/validators.go | 4 +- vdr/didx509/resolver_test.go | 2 +- vdr/didx509/x509_utils.go | 2 +- vdr/didx509/x509_utils_test.go | 36 ++--- vdr/resolver/did.go | 2 +- vdr/resolver/did_test.go | 2 +- vdr/vdr_test.go | 4 +- 105 files changed, 967 insertions(+), 727 deletions(-) create mode 100644 auth/api/iam/params_test.go create mode 100644 crypto/jwx/claims.go rename crypto/{jwx => }/jwx_es256k_test.go (52%) 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/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 ba92fd2d35..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 @@ -521,14 +528,25 @@ 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() - result.IssuedAt = token.IssuedAt().Unix() - result.Expiration = token.Expiration().Unix() + result.Subject, _ = token.Subject() + result.Issuer, _ = token.Issuer() + iat, _ := token.IssuedAt() + result.IssuedAt = iat.Unix() + exp, _ := token.Expiration() + result.Expiration = exp.Unix() // Validate required claims if result.Issuer == "" { diff --git a/auth/services/oauth/authz_server_test.go b/auth/services/oauth/authz_server_test.go index a9be7149d2..89d2bf2ff9 100644 --- a/auth/services/oauth/authz_server_test.go +++ b/auth/services/oauth/authz_server_test.go @@ -23,7 +23,6 @@ import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" - "crypto/rsa" "encoding/json" "errors" "fmt" @@ -32,9 +31,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" @@ -517,7 +516,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,30 +562,26 @@ 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) { - t.Setenv("GODEBUG", "rsa1024min=0") // minimum key-length has changed to 1024 -> https://pkg.go.dev/crypto/rsa#hdr-Minimum_key_size - privateKey, err := rsa.GenerateKey(rand.Reader, 512) - require.NoError(t, err) - keyID := "did:nuts:somedid#key-id" + secret := []byte("test-hmac-secret") - ctx.keyResolver.EXPECT().ResolveKeyByID(keyID, nil, resolver.NutsSigningKeyType).Return(privateKey.Public(), nil) + ctx.keyResolver.EXPECT().ResolveKeyByID(keyID, nil, resolver.NutsSigningKeyType).Return(secret, nil) - // alg: RS256 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.HS256(), secret, jws.WithProtectedHeaders(hdrs))) require.NoError(t, err) tokenCtx := &validationContext{ @@ -594,7 +589,7 @@ func TestService_parseAndValidateJwtBearerToken(t *testing.T) { } err = ctx.oauthService.parseAndValidateJwtBearerToken(tokenCtx) assert.Nil(t, tokenCtx.jwtBearerToken) - assert.Equal(t, "token signing algorithm is not supported: RS256", err.Error()) + assert.Equal(t, "token signing algorithm is not supported: HS256", err.Error()) }) t.Run("valid token", func(t *testing.T) { @@ -605,7 +600,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 +632,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") }) } @@ -698,10 +694,14 @@ func TestService_IntrospectAccessToken(t *testing.T) { 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) }) @@ -719,7 +719,7 @@ func TestService_IntrospectAccessToken(t *testing.T) { // 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) }) @@ -956,7 +956,7 @@ func signTokenWithKeyAndHeaders(context *validationContext, key *ecdsa.PrivateKe panic(err) } } - signedToken, err := jwt.Sign(context.jwtBearerToken, jwt.WithKey(jwa.ES256, key, jws.WithProtectedHeaders(hdrs))) + 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 6eb8a25f7d..39dde1ff3c 100644 --- a/core/echo_errors.go +++ b/core/echo_errors.go @@ -193,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/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/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 bfa83817b8..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" diff --git a/discovery/store.go b/discovery/store.go index be65f92113..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{} 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/go.mod b/go.mod index 3a4feff968..d442dbc6c9 100644 --- a/go.mod +++ b/go.mod @@ -24,14 +24,13 @@ 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.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.1 github.com/nuts-foundation/sqlite v1.0.0 @@ -53,7 +52,7 @@ 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.52.0 + 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 @@ -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 @@ -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 @@ -130,11 +129,12 @@ require ( 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,7 +149,7 @@ 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.1 // 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 @@ -185,10 +185,10 @@ require ( github.com/yuin/gopher-lua v1.1.1 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/net v0.55.0 // indirect - golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.45.0 // indirect - golang.org/x/term v0.43.0 // indirect - golang.org/x/text v0.37.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 @@ -208,6 +208,7 @@ 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 @@ -248,6 +249,7 @@ 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.43.0 // indirect go.opentelemetry.io/otel/log v0.19.0 // indirect diff --git a/go.sum b/go.sum index 3885b92356..d3ab1a2601 100644 --- a/go.sum +++ b/go.sum @@ -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= @@ -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= @@ -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,14 +368,14 @@ 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= @@ -396,8 +398,8 @@ 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.1 h1:ZQOeRKzC1+AfW6Ve5kBJMo+zYhHLBUI4KrLWKYkxoeI= @@ -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= @@ -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.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= -golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= +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.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= -golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= +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= @@ -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.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +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.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= -golang.org/x/sys v0.45.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.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= -golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= +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,8 +721,8 @@ 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.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +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= @@ -730,8 +734,8 @@ 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.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= -golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= +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= 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/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/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) From 4966668109ee330d23404fff7f64e1200a3adf1f Mon Sep 17 00:00:00 2001 From: Rein Krul Date: Wed, 15 Jul 2026 14:50:30 +0200 Subject: [PATCH 2/2] fixup: preserve V6.2's original RSA-based algorithm-rejection test; fix v3 error-string drift in openid4vci_test.go The RSA512->HS256 swap in authz_server_test.go's "wrong signing algorithm" test was copied from master, but master's swap was caused by an unrelated, already-excluded commit (RS256 becoming a supported algorithm, #4252) rather than by jwx v3 itself. jwx v3 signs fine with a 512-bit RSA key (verified empirically), so V6.2's original RSA/RS256 test works unchanged aside from the jwa.RS256 -> jwa.RS256() syntax translation - restored it to keep RS256 (still unsupported on V6.2) under test instead of an unrelated algorithm. Also fixes an untouched-by-conflict v2-style error string assertion in auth/api/iam/openid4vci_test.go, caught by a full -count=1 test run. Assisted by AI --- auth/api/iam/openid4vci_test.go | 2 +- auth/services/oauth/authz_server_test.go | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) 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/services/oauth/authz_server_test.go b/auth/services/oauth/authz_server_test.go index 89d2bf2ff9..23f266f70b 100644 --- a/auth/services/oauth/authz_server_test.go +++ b/auth/services/oauth/authz_server_test.go @@ -23,6 +23,7 @@ import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" + "crypto/rsa" "encoding/json" "errors" "fmt" @@ -573,15 +574,19 @@ func TestService_parseAndValidateJwtBearerToken(t *testing.T) { }) t.Run("wrong signing algorithm", func(t *testing.T) { + t.Setenv("GODEBUG", "rsa1024min=0") // minimum key-length has changed to 1024 -> https://pkg.go.dev/crypto/rsa#hdr-Minimum_key_size + privateKey, err := rsa.GenerateKey(rand.Reader, 512) + require.NoError(t, err) + keyID := "did:nuts:somedid#key-id" - secret := []byte("test-hmac-secret") - ctx.keyResolver.EXPECT().ResolveKeyByID(keyID, nil, resolver.NutsSigningKeyType).Return(secret, nil) + ctx.keyResolver.EXPECT().ResolveKeyByID(keyID, nil, resolver.NutsSigningKeyType).Return(privateKey.Public(), nil) + // alg: RS256 token := jwt.New() hdrs := jws.NewHeaders() hdrs.Set(jws.KeyIDKey, keyID) - signedToken, err := jwt.Sign(token, jwt.WithKey(jwa.HS256(), secret, jws.WithProtectedHeaders(hdrs))) + signedToken, err := jwt.Sign(token, jwt.WithKey(jwa.RS256(), privateKey, jws.WithProtectedHeaders(hdrs))) require.NoError(t, err) tokenCtx := &validationContext{ @@ -589,7 +594,7 @@ func TestService_parseAndValidateJwtBearerToken(t *testing.T) { } err = ctx.oauthService.parseAndValidateJwtBearerToken(tokenCtx) assert.Nil(t, tokenCtx.jwtBearerToken) - assert.Equal(t, "token signing algorithm is not supported: HS256", err.Error()) + assert.Equal(t, "token signing algorithm is not supported: RS256", err.Error()) }) t.Run("valid token", func(t *testing.T) {