diff --git a/auth/api/iam/api.go b/auth/api/iam/api.go index 6386a3bc22..3ce7eb578e 100644 --- a/auth/api/iam/api.go +++ b/auth/api/iam/api.go @@ -39,8 +39,8 @@ import ( "github.com/nuts-foundation/nuts-node/core/to" "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" @@ -402,7 +402,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} } @@ -653,7 +657,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 01a1e031af..41c39b3782 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" @@ -1538,7 +1538,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 e634ed9c32..50970afff6 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" "time" @@ -183,7 +184,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} @@ -213,7 +214,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 17b5741e2d..ef039ec8ac 100644 --- a/auth/api/iam/jar_test.go +++ b/auth/api/iam/jar_test.go @@ -23,15 +23,15 @@ 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" "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" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/nuts-node/auth" "github.com/nuts-foundation/nuts-node/auth/client/iam" @@ -126,7 +126,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) @@ -329,7 +329,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) @@ -358,7 +358,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 c5817da2c8..453bc4183c 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 8db40857bb..1afcfcf460 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 71e5fb4b17..90df21a7af 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 34212badbf..c60a879926 100644 --- a/auth/api/iam/s2s_vptoken.go +++ b/auth/api/iam/s2s_vptoken.go @@ -226,8 +226,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 bb07c86ae4..f76e1e5c1a 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 81c3c66596..901abe3d35 100644 --- a/auth/api/iam/validation.go +++ b/auth/api/iam/validation.go @@ -54,7 +54,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 8c4ef901bc..3494c827cb 100644 --- a/auth/client/iam/client.go +++ b/auth/client/iam/client.go @@ -29,9 +29,10 @@ import ( "strings" "time" - "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" "github.com/nuts-foundation/go-did/vc" @@ -260,12 +261,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) @@ -275,7 +277,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 f9c89dddaa..c02bfbc2d8 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" @@ -280,6 +281,7 @@ func TestHTTPClient_OpenIDConfiguration(t *testing.T) { ctx := context.Background() configuration := oauth.OpenIDConfiguration{ Issuer: "issuer", + JWKs: jwk.NewSet(), } // create jwt @@ -288,6 +290,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) @@ -335,7 +340,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{ @@ -349,7 +354,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 e1e3d8d580..97be477427 100644 --- a/auth/oauth/types.go +++ b/auth/oauth/types.go @@ -23,7 +23,7 @@ import ( "encoding/json" "net/url" - "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v3/jwk" "github.com/nuts-foundation/nuts-node/core" ) diff --git a/auth/oauth/types_test.go b/auth/oauth/types_test.go index 4aa4e8877f..9e6f22351a 100644 --- a/auth/oauth/types_test.go +++ b/auth/oauth/types_test.go @@ -121,7 +121,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 469d68be66..76f5971451 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 } @@ -228,8 +229,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). @@ -336,18 +337,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 @@ -357,13 +360,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, } @@ -372,7 +376,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}, } @@ -408,17 +412,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 @@ -459,9 +464,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 @@ -519,14 +524,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() return result, nil } diff --git a/auth/services/oauth/authz_server_test.go b/auth/services/oauth/authz_server_test.go index 7e1141dacd..c9c8ab71ff 100644 --- a/auth/services/oauth/authz_server_test.go +++ b/auth/services/oauth/authz_server_test.go @@ -31,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 +517,7 @@ func TestService_validateAuthorizationCredentials(t *testing.T) { err := ctx.oauthService.validateAuthorizationCredentials(tokenCtx) - assert.EqualError(t, err, "invalid jwt.vcs: cannot unmarshal authorization credential: invalid JWT") + assert.EqualError(t, err, "invalid jwt.vcs: cannot unmarshal authorization credential: jwt.Parse: failed to parse token: unknown payload type (payload is not JWT?)") }) t.Run("error - jwt.iss <> credentialSubject.ID mismatch", func(t *testing.T) { @@ -563,14 +563,14 @@ func TestService_parseAndValidateJwtBearerToken(t *testing.T) { } err := ctx.oauthService.parseAndValidateJwtBearerToken(tokenCtx) assert.Nil(t, tokenCtx.jwtBearerToken) - assert.Equal(t, "invalid compact serialization format: invalid number of segments", err.Error()) + assert.Equal(t, "jws.ParseString: failed to parse string: jws.Parse: failed to parse compact format: jws.Parse: invalid compact serialization format: jwsbb: invalid number of segments", err.Error()) tokenCtx2 := &validationContext{ rawJwtBearerToken: "123.456.787", } err = ctx.oauthService.parseAndValidateJwtBearerToken(tokenCtx2) assert.Nil(t, tokenCtx.jwtBearerToken) - assert.Equal(t, "failed to parse JOSE headers: invalid character '×' looking for beginning of value", err.Error()) + assert.Equal(t, "jws.ParseString: failed to parse string: jws.Parse: failed to parse compact format: failed to parse JOSE headers: invalid character '×' looking for beginning of value", err.Error()) }) t.Run("wrong signing algorithm", func(t *testing.T) { @@ -582,7 +582,7 @@ func TestService_parseAndValidateJwtBearerToken(t *testing.T) { token := jwt.New() hdrs := jws.NewHeaders() hdrs.Set(jws.KeyIDKey, keyID) - signedToken, err := jwt.Sign(token, jwt.WithKey(jwa.HS256, secret, jws.WithProtectedHeaders(hdrs))) + signedToken, err := jwt.Sign(token, jwt.WithKey(jwa.HS256(), secret, jws.WithProtectedHeaders(hdrs))) require.NoError(t, err) tokenCtx := &validationContext{ @@ -601,7 +601,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) }) @@ -633,7 +634,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") }) } @@ -695,10 +696,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) }) @@ -716,7 +721,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) }) @@ -877,7 +882,7 @@ func TestService_IntrospectAccessToken(t *testing.T) { signTokenWithKeyAndHeaders(tokenCtx, authorizerSigningKey, authorizerSigningKeyID, map[string]interface{}{"typ": "at+jwt"}) _, err := ctx.oauthService.IntrospectAccessToken(ctx.audit, tokenCtx.rawJwtBearerToken) - assert.ErrorContains(t, err, `"exp" not satisfied: required claim not found`) + assert.ErrorContains(t, err, `required claim "exp" is missing`) }) t.Run("rejects token without iat claim", func(t *testing.T) { @@ -900,7 +905,7 @@ func TestService_IntrospectAccessToken(t *testing.T) { signTokenWithKeyAndHeaders(tokenCtx, authorizerSigningKey, authorizerSigningKeyID, map[string]interface{}{"typ": "at+jwt"}) _, err := ctx.oauthService.IntrospectAccessToken(ctx.audit, tokenCtx.rawJwtBearerToken) - assert.ErrorContains(t, err, `"iat" not satisfied: required claim not found`) + assert.ErrorContains(t, err, `required claim "iat" is missing`) }) t.Run("rejects token with excessive lifetime", func(t *testing.T) { @@ -1093,7 +1098,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/jwt_profile.go b/crypto/jwt_profile.go index 4eae45cb6c..19c95dd3cd 100644 --- a/crypto/jwt_profile.go +++ b/crypto/jwt_profile.go @@ -21,7 +21,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" ) @@ -77,8 +77,9 @@ func IssuerKidValidator(token jwt.Token, headers map[string]interface{}) error { if err != nil { return fmt.Errorf("invalid kid header: %w", err) } - if parsed.DID.String() != token.Issuer() { - return fmt.Errorf("token issuer (%s) does not match signing key DID (%s)", token.Issuer(), parsed.DID.String()) + iss, _ := token.Issuer() + if parsed.DID.String() != iss { + return fmt.Errorf("token issuer (%s) does not match signing key DID (%s)", iss, parsed.DID.String()) } return nil } diff --git a/crypto/jwx.go b/crypto/jwx.go index 959cc82010..ea639d2485 100644 --- a/crypto/jwx.go +++ b/crypto/jwx.go @@ -30,11 +30,11 @@ import ( "maps" "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/jwx" @@ -49,11 +49,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. @@ -101,7 +101,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") } @@ -112,18 +112,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 } @@ -144,7 +142,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 @@ -154,16 +152,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 @@ -242,8 +242,8 @@ func ParseJWT(tokenString string, f PublicKeyFunc, profile *JWTProfile, at *time // Check required claims are present and non-empty. // Mirrors the jwx error format ("" not satisfied: ...) so callers see one consistent style. for _, claim := range profile.RequiredClaims { - val, ok := token.Get(claim) - if !ok { + var val interface{} + if err := token.Get(claim, &val); err != nil { return nil, fmt.Errorf(`%q not satisfied: required claim not found`, claim) } if s, isStr := val.(string); isStr && s == "" { @@ -277,17 +277,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 @@ -298,10 +293,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() @@ -316,7 +314,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) @@ -324,7 +322,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") @@ -357,19 +355,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 { @@ -379,14 +377,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...) @@ -405,16 +404,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 { @@ -444,24 +466,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 } @@ -471,7 +493,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{} @@ -490,19 +512,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) } } @@ -514,7 +536,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 bbf5eb54c9..c17a46574f 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, jwa.RS256} +var SupportedAlgorithms = []jwa.SignatureAlgorithm{jwa.ES256(), jwa.EdDSA(), jwa.ES384(), jwa.ES512(), jwa.PS256(), jwa.PS384(), jwa.PS512(), jwa.RS256()} -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 51% rename from crypto/jwx/jwx_es256k_test.go rename to crypto/jwx_es256k_test.go index cfc99dff88..a18e866a2c 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 - }, 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 + }, nil, nil) + require.NoError(t, err) + assert.NotNil(t, parsedToken) } diff --git a/crypto/jwx_test.go b/crypto/jwx_test.go index 6866906fb2..a5216f8280 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) { }, nil, nil) 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) { secret := []byte("test-hmac-secret") token := jwt.New() - signature, _ := jwt.Sign(token, jwt.WithKey(jwa.HS256, secret)) + signature, _ := jwt.Sign(token, jwt.WithKey(jwa.HS256(), secret)) parsedToken, err := ParseJWT(string(signature), func(_ string) (crypto.PublicKey, error) { return secret, nil }, nil, nil) @@ -136,7 +139,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 }, nil, nil) @@ -151,7 +154,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)) _, err = ParseJWT(string(signature), func(_ string) (crypto.PublicKey, error) { return ecKey.Public(), nil }, (&JWTProfile{}).WithClockSkew(1*time.Second), nil) @@ -162,14 +165,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 }, nil, 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") }) } @@ -192,7 +195,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) { @@ -217,7 +221,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) { @@ -299,7 +304,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, @@ -311,7 +316,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) { @@ -375,7 +381,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{}) @@ -507,19 +513,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) @@ -532,13 +540,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") @@ -568,7 +576,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) { @@ -577,7 +585,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) }) @@ -594,9 +603,9 @@ func TestCrypto_convertHeaders(t *testing.T) { } func Test_isAlgorithmSupported(t *testing.T) { - assert.True(t, jwx.IsAlgorithmSupported(jwa.PS256)) - assert.True(t, jwx.IsAlgorithmSupported(jwa.RS256)) - assert.False(t, jwx.IsAlgorithmSupported("")) + assert.True(t, jwx.IsAlgorithmSupported(jwa.PS256())) + assert.True(t, jwx.IsAlgorithmSupported(jwa.RS256())) + assert.False(t, jwx.IsAlgorithmSupported(jwa.SignatureAlgorithm{})) } func TestSignatureAlgorithm(t *testing.T) { @@ -624,16 +633,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 { @@ -695,33 +704,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) @@ -740,7 +749,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 } @@ -755,7 +764,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 9078611bf6..752e431e11 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 3556ffd009..04f7282418 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.5 github.com/labstack/echo/v4 v4.15.4 - github.com/lestrrat-go/jwx/v2 v2.1.7 github.com/mdp/qrterminal/v3 v3.2.1 github.com/mr-tron/base58 v1.3.0 github.com/multiformats/go-multicodec v0.10.0 github.com/nats-io/nats-server/v2 v2.14.3 github.com/nats-io/nats.go v1.52.0 github.com/nuts-foundation/crypto-ecies v0.0.0-20211207143025-5b84f9efce2b - github.com/nuts-foundation/go-did v0.21.0 + github.com/nuts-foundation/go-did v0.22.0 github.com/nuts-foundation/go-leia/v4 v4.3.0 github.com/nuts-foundation/go-stoabs v1.11.2 github.com/nuts-foundation/sqlite v1.0.0 @@ -132,9 +131,6 @@ require ( github.com/labstack/gommon v0.5.0 // indirect github.com/lestrrat-go/blackmagic v1.0.4 // 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/mattn/go-colorable v0.1.15 // indirect github.com/mattn/go-isatty v0.0.22 // indirect github.com/mfridman/interpolate v0.0.2 // indirect @@ -149,7 +145,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.2 // indirect @@ -208,6 +204,7 @@ require ( github.com/eko/gocache/store/go_cache/v4 v4.2.5 github.com/eko/gocache/store/memcache/v4 v4.2.4 github.com/eko/gocache/store/redis/v4 v4.2.6 + 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.19.0 @@ -245,12 +242,13 @@ require ( github.com/google/go-tpm v0.9.8 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect - github.com/lestrrat-go/httprc/v3 v3.0.0 // indirect - github.com/lestrrat-go/jwx/v3 v3.0.10 // indirect + github.com/lestrrat-go/dsig v1.2.1 // indirect + github.com/lestrrat-go/dsig-secp256k1 v1.0.0 // indirect + github.com/lestrrat-go/httprc/v3 v3.0.5 // indirect github.com/lestrrat-go/option/v2 v2.0.0 // 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.4 // 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.44.0 // indirect go.opentelemetry.io/otel/log v0.20.0 // indirect diff --git a/go.sum b/go.sum index e83d776b0e..39fdade3df 100644 --- a/go.sum +++ b/go.sum @@ -323,20 +323,16 @@ github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kUL github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= 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/httprc/v3 v3.0.0 h1:nZUx/zFg5uc2rhlu1L1DidGr5Sj02JbXvGSpnY4LMrc= -github.com/lestrrat-go/httprc/v3 v3.0.0/go.mod h1:k2U1QIiyVqAKtkffbg+cUmsyiPGQsb9aAfNQiNFuQ9Q= -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.7 h1:bnYeET+S8IOyAw6W4LTc6SEeK7Xs58SKKZkR7scb3Ko= -github.com/lestrrat-go/jwx/v2 v2.1.7/go.mod h1:exQ9ZBuN1cMLYmxwhTlHUru08ykONG0z+HbLEeDG9qo= -github.com/lestrrat-go/jwx/v3 v3.0.10 h1:XuoCBhZBncRIjMQ32HdEc76rH0xK/Qv2wq5TBouYJDw= -github.com/lestrrat-go/jwx/v3 v3.0.10/go.mod h1:kNMedLgTpHvPJkK5EMVa1JFz+UVyY2dMmZKu3qjl/Pk= -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.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY= @@ -378,8 +374,8 @@ github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aG 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= @@ -402,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.21.0 h1:jQAr6O8afgVp88Rkwnfl1m5E0b0L0KTkSqM5iDnq0fw= -github.com/nuts-foundation/go-did v0.21.0/go.mod h1:4od1gAmCi9HjHTQGEvHC8pLeuXdXACxidAcdA52YScc= +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.3.0 h1:R0qGISIeg2q/PCQTC9cuoBtA6cFu4WBV2DbmSOWKZyM= github.com/nuts-foundation/go-leia/v4 v4.3.0/go.mod h1:Gw6bXqJLOAmHSiXJJYbVoj+Mowp/PoBRywO0ZPsVzA0= github.com/nuts-foundation/go-stoabs v1.11.2 h1:cOYjtozhdmDQDPNS2STBly5hp0XomNIOv6TqUHKeEEg= @@ -540,8 +536,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.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ= -github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= +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= 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 fd0ae4ba08..f8a9dda3e4 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_test.go b/vcr/holder/presenter_test.go index 3c65a77e54..899945b428 100644 --- a/vcr/holder/presenter_test.go +++ b/vcr/holder/presenter_test.go @@ -161,7 +161,8 @@ 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("#3957: Verifiable Presentation type is marshalled incorrectly in JWT format", func(t *testing.T) { @@ -175,7 +176,9 @@ func TestPresenter_buildPresentation(t *testing.T) { assert.Contains(t, string(data), `"type":"VerifiablePresentation"`) }) }) - vpAsMap := result.JWT().PrivateClaims()["vp"].(map[string]any) + var vpClaim interface{} + _ = result.JWT().Get("vp", &vpClaim) + vpAsMap := vpClaim.(map[string]any) t.Run("make sure type now marshals as array", func(t *testing.T) { typeProp := vpAsMap["type"].([]any) assert.Equal(t, []any{"VerifiablePresentation"}, typeProp) @@ -204,8 +207,11 @@ func TestPresenter_buildPresentation(t *testing.T) { require.NoError(t, err) require.NotNil(t, result) - assert.Equal(t, testDID.String(), result.JWT().Issuer(), "holder must be carried in the iss claim") - vpAsMap := result.JWT().PrivateClaims()["vp"].(map[string]any) + iss, _ := result.JWT().Issuer() + assert.Equal(t, testDID.String(), iss, "holder must be carried in the iss claim") + var vpClaim interface{} + _ = result.JWT().Get("vp", &vpClaim) + vpAsMap := vpClaim.(map[string]any) assert.NotContains(t, vpAsMap, "holder", "non-standard vp.holder must not be set") }) @@ -255,12 +261,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 c221083194..7af39befda 100644 --- a/vcr/issuer/openid.go +++ b/vcr/issuer/openid.go @@ -25,7 +25,7 @@ import ( "errors" "fmt" "github.com/google/uuid" - "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" @@ -333,8 +333,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 @@ -342,15 +343,15 @@ 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, }) } // 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, @@ -359,7 +360,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/pe/presentation_definition.go b/vcr/pe/presentation_definition.go index f204463f13..2610e332fa 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" @@ -350,7 +349,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 { @@ -359,7 +358,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 0d8e19068d..71b64b8069 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) @@ -547,8 +548,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 2c8fd21ab1..41cb2bd645 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" ) @@ -165,11 +165,13 @@ func vpAsInterface(presentation vc.VerifiablePresentation) (interface{}, error) } 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, 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 b900269418..53cbcd6a0d 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 4d3bc4f7d9..51f01a2e4e 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" @@ -471,7 +471,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") }) } @@ -488,7 +488,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` @@ -529,7 +529,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) @@ -566,7 +567,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) { @@ -729,7 +730,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 408e60562f..ba827d6c99 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)