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)