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