From 1ed6c5c24ed20b8d2b836b7a7302e23710e59242 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Oct 2023 09:25:30 +0200 Subject: [PATCH 01/23] Bump github.com/prometheus/client_golang from 1.16.0 to 1.17.0 (#2519) Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.16.0 to 1.17.0. - [Release notes](https://github.com/prometheus/client_golang/releases) - [Changelog](https://github.com/prometheus/client_golang/blob/v1.17.0/CHANGELOG.md) - [Commits](https://github.com/prometheus/client_golang/compare/v1.16.0...v1.17.0) --- updated-dependencies: - dependency-name: github.com/prometheus/client_golang dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 8 ++++---- go.sum | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 3f75f649d0..efd15ced4b 100644 --- a/go.mod +++ b/go.mod @@ -28,8 +28,8 @@ require ( github.com/oapi-codegen/runtime v1.0.0 github.com/piprate/json-gold v0.5.1-0.20230111113000-6ddbe6e6f19f github.com/privacybydesign/irmago v0.12.6 - github.com/prometheus/client_golang v1.16.0 - github.com/prometheus/client_model v0.4.0 + github.com/prometheus/client_golang v1.17.0 + github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 github.com/redis/go-redis/v9 v9.2.1 github.com/shengdoushi/base58 v1.0.0 github.com/sirupsen/logrus v1.9.3 @@ -133,8 +133,8 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect github.com/privacybydesign/gabi v0.0.0-20221012093643-8e978bfbb252 // indirect - github.com/prometheus/common v0.42.0 // indirect - github.com/prometheus/procfs v0.10.1 // indirect + github.com/prometheus/common v0.44.0 // indirect + github.com/prometheus/procfs v0.11.1 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect diff --git a/go.sum b/go.sum index ad8991a683..6619b439a5 100644 --- a/go.sum +++ b/go.sum @@ -488,25 +488,25 @@ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= -github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= +github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= +github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= -github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= +github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM= +github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= -github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= +github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= +github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= -github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= +github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI= +github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY= github.com/redis/go-redis/v9 v9.0.2/go.mod h1:/xDTe9EF1LM61hek62Poq2nzQSGj0xSrEtEHbBQevps= github.com/redis/go-redis/v9 v9.2.1 h1:WlYJg71ODF0dVspZZCpYmoF1+U1Jjk9Rwd7pq6QmlCg= github.com/redis/go-redis/v9 v9.2.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= From 7b114732ede3e285b508436f05c424d84e7ef8a8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Oct 2023 11:22:19 +0200 Subject: [PATCH 02/23] Bump github.com/prometheus/client_model from 0.4.0 to 0.5.0 (#2527) Bumps [github.com/prometheus/client_model](https://github.com/prometheus/client_model) from 0.4.0 to 0.5.0. - [Release notes](https://github.com/prometheus/client_model/releases) - [Commits](https://github.com/prometheus/client_model/compare/v0.4.0...v0.5.0) --- updated-dependencies: - dependency-name: github.com/prometheus/client_model dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index efd15ced4b..91a949dbaa 100644 --- a/go.mod +++ b/go.mod @@ -29,7 +29,7 @@ require ( github.com/piprate/json-gold v0.5.1-0.20230111113000-6ddbe6e6f19f github.com/privacybydesign/irmago v0.12.6 github.com/prometheus/client_golang v1.17.0 - github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 + github.com/prometheus/client_model v0.5.0 github.com/redis/go-redis/v9 v9.2.1 github.com/shengdoushi/base58 v1.0.0 github.com/sirupsen/logrus v1.9.3 diff --git a/go.sum b/go.sum index 6619b439a5..2d86d77e34 100644 --- a/go.sum +++ b/go.sum @@ -494,8 +494,8 @@ github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1: github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM= -github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= From 86557dd154a4ce6554bc96a4d7126fa8ed3e955b Mon Sep 17 00:00:00 2001 From: reinkrul Date: Thu, 5 Oct 2023 19:05:29 +0200 Subject: [PATCH 03/23] IAM: Use SessionDatabase in OpenID4VP (#2525) --- auth/api/iam/api.go | 24 +++---- auth/api/iam/authorized_code.go | 115 -------------------------------- auth/api/iam/openid4vp.go | 25 +++++-- auth/api/iam/openid4vp_test.go | 9 +-- auth/api/iam/session.go | 24 ------- cmd/root.go | 2 +- 6 files changed, 37 insertions(+), 162 deletions(-) delete mode 100644 auth/api/iam/authorized_code.go diff --git a/auth/api/iam/api.go b/auth/api/iam/api.go index ce87ed81d9..8c9a9b8db5 100644 --- a/auth/api/iam/api.go +++ b/auth/api/iam/api.go @@ -28,13 +28,13 @@ import ( "github.com/nuts-foundation/nuts-node/auth" "github.com/nuts-foundation/nuts-node/auth/log" "github.com/nuts-foundation/nuts-node/core" + "github.com/nuts-foundation/nuts-node/storage" "github.com/nuts-foundation/nuts-node/vcr" "github.com/nuts-foundation/nuts-node/vdr" "github.com/nuts-foundation/nuts-node/vdr/resolver" "html/template" "net/http" "strings" - "sync" ) var _ core.Routable = &Wrapper{} @@ -49,25 +49,25 @@ var assets embed.FS // Wrapper handles OAuth2 flows. type Wrapper struct { - vcr vcr.VCR - vdr vdr.VDR - auth auth.AuthenticationServices - sessions *SessionManager - templates *template.Template + vcr vcr.VCR + vdr vdr.VDR + auth auth.AuthenticationServices + templates *template.Template + storageEngine storage.Engine } -func New(authInstance auth.AuthenticationServices, vcrInstance vcr.VCR, vdrInstance vdr.VDR) *Wrapper { +func New(authInstance auth.AuthenticationServices, vcrInstance vcr.VCR, vdrInstance vdr.VDR, storageEngine storage.Engine) *Wrapper { templates := template.New("oauth2 templates") _, err := templates.ParseFS(assets, "assets/*.html") if err != nil { panic(err) } return &Wrapper{ - sessions: &SessionManager{sessions: new(sync.Map)}, - auth: authInstance, - vcr: vcrInstance, - vdr: vdrInstance, - templates: templates, + storageEngine: storageEngine, + auth: authInstance, + vcr: vcrInstance, + vdr: vdrInstance, + templates: templates, } } diff --git a/auth/api/iam/authorized_code.go b/auth/api/iam/authorized_code.go deleted file mode 100644 index 2e4cc2521b..0000000000 --- a/auth/api/iam/authorized_code.go +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright (C) 2023 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 ( - "bytes" - "crypto/rand" - "encoding/base64" - "errors" - "fmt" - "github.com/labstack/echo/v4" - "github.com/nuts-foundation/nuts-node/core" - "html/template" - "net/http" - "net/url" -) - -func newAuthorizedCodeFlow(sessions *SessionManager) *authorizedCodeFlow { - authzTemplate, _ := template.ParseFS(assets, "assets/authz_en.html") - return &authorizedCodeFlow{ - sessions: sessions, - authzTemplate: authzTemplate, - } -} - -// authorizedCodeFlow implements the grant type as specified by https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3. -type authorizedCodeFlow struct { - sessions *SessionManager - authzTemplate *template.Template -} - -func (a authorizedCodeFlow) Routes(router core.EchoRouter) { - router.Add(http.MethodPost, "/public/oauth2/:did/authz_consent", a.handleAuthConsent) -} - -func (a authorizedCodeFlow) handleAuthzRequest(params map[string]string, session *Session) (*authzResponse, error) { - // This authz request handling is just for demonstration purposes. - sessionId := a.sessions.Create(*session) - - // Render HTML - buf := new(bytes.Buffer) - // TODO: Support multiple languages - err := a.authzTemplate.Execute(buf, struct { - SessionID string - Session - }{ - SessionID: sessionId, - Session: *session, - }) - if err != nil { - return nil, fmt.Errorf("unable to render authorization page: %w", err) - } - return &authzResponse{ - html: buf.Bytes(), - }, nil -} - -// handleAuthConsent handles the authorization consent form submission. -func (a authorizedCodeFlow) handleAuthConsent(c echo.Context) error { - var session *Session - if sessionID := c.Param("sessionID"); sessionID != "" { - session = a.sessions.Get(sessionID) - } - if session == nil { - return errors.New("invalid session") - } - - redirectURI, _ := url.Parse(session.RedirectURI) // Validated on session creation, can't fail - query := redirectURI.Query() - query.Add("code", generateCode()) - redirectURI.RawQuery = query.Encode() - - return c.Redirect(http.StatusFound, redirectURI.String()) -} - -func (a authorizedCodeFlow) validateCode(params map[string]string) (string, error) { - code, ok := params["code"] - invalidCodeError := OAuth2Error{ - Code: InvalidRequest, - Description: "missing or invalid code parameter", - } - if !ok { - return "", invalidCodeError - } - session := a.sessions.Get(code) - if session == nil { - return "", invalidCodeError - } - return session.Scope, nil -} - -func generateCode() string { - buf := make([]byte, 128/8) - _, err := rand.Read(buf) - if err != nil { - panic(err) - } - return base64.URLEncoding.EncodeToString(buf) -} diff --git a/auth/api/iam/openid4vp.go b/auth/api/iam/openid4vp.go index 30f467a85a..b282b4f5f8 100644 --- a/auth/api/iam/openid4vp.go +++ b/auth/api/iam/openid4vp.go @@ -24,6 +24,7 @@ import ( "encoding/json" "errors" "fmt" + "github.com/google/uuid" "github.com/labstack/echo/v4" ssi "github.com/nuts-foundation/go-did" "github.com/nuts-foundation/go-did/did" @@ -33,8 +34,11 @@ import ( "net/http" "net/url" "strings" + "time" ) +const sessionExpiry = 5 * time.Minute + // createPresentationRequest creates a new Authorization Request as specified by OpenID4VP: https://openid.net/specs/openid-4-verifiable-presentations-1_0.html. // It is sent by a verifier to a wallet, to request one or more verifiable credentials as verifiable presentation from the wallet. func (r *Wrapper) sendPresentationRequest(ctx context.Context, response http.ResponseWriter, scope string, @@ -145,7 +149,12 @@ func (r *Wrapper) handlePresentationRequest(params map[string]string, session *S } session.ServerState["openid4vp_credentials"] = credentialIDs - templateParams.SessionID = r.sessions.Create(*session) + sessionID := uuid.NewString() + err = r.storageEngine.GetSessionDatabase().GetStore(sessionExpiry, session.OwnDID.String(), "session").Put(sessionID, *session) + if err != nil { + return nil, err + } + templateParams.SessionID = sessionID // TODO: Support multiple languages buf := new(bytes.Buffer) @@ -162,12 +171,16 @@ func (r *Wrapper) handlePresentationRequest(params map[string]string, session *S // handleAuthConsent handles the authorization consent form submission. func (r *Wrapper) handlePresentationRequestAccept(c echo.Context) error { // TODO: Needs authentication? - var session *Session - if sessionID := c.FormValue("sessionID"); sessionID != "" { - session = r.sessions.Get(sessionID) + sessionID := c.FormValue("sessionID") + if sessionID == "" { + return errors.New("missing sessionID parameter") } - if session == nil { - return errors.New("invalid session") + + var session Session + sessionStore := r.storageEngine.GetSessionDatabase().GetStore(sessionExpiry, "openid", session.OwnDID.String(), "session") + err := sessionStore.Get(sessionID, &session) + if err != nil { + return fmt.Errorf("invalid session: %w", err) } // TODO: Change to loading from wallet diff --git a/auth/api/iam/openid4vp_test.go b/auth/api/iam/openid4vp_test.go index 91fec61bfd..22920d4a92 100644 --- a/auth/api/iam/openid4vp_test.go +++ b/auth/api/iam/openid4vp_test.go @@ -25,6 +25,7 @@ import ( "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/go-did/vc" "github.com/nuts-foundation/nuts-node/auth" + "github.com/nuts-foundation/nuts-node/storage" "github.com/nuts-foundation/nuts-node/vcr" "github.com/nuts-foundation/nuts-node/vcr/credential" "github.com/nuts-foundation/nuts-node/vcr/holder" @@ -42,7 +43,7 @@ var holderDID = did.MustParseDID("did:web:example.com:holder") var issuerDID = did.MustParseDID("did:web:example.com:issuer") func TestWrapper_sendPresentationRequest(t *testing.T) { - instance := New(nil, nil, nil) + instance := New(nil, nil, nil, nil) redirectURI, _ := url.Parse("https://example.com/redirect") verifierID, _ := url.Parse("https://example.com/verifier") @@ -101,7 +102,7 @@ func TestWrapper_handlePresentationRequest(t *testing.T) { mockAuth.EXPECT().PresentationDefinitions().Return(peStore) mockWallet.EXPECT().List(gomock.Any(), holderDID).Return(walletCredentials, nil) mockVDR.EXPECT().IsOwner(gomock.Any(), holderDID).Return(true, nil) - instance := New(mockAuth, mockVCR, mockVDR) + instance := New(mockAuth, mockVCR, mockVDR, storage.NewTestStorageEngine(t)) params := map[string]string{ "scope": "eOverdracht-overdrachtsbericht", @@ -124,7 +125,7 @@ func TestWrapper_handlePresentationRequest(t *testing.T) { _ = peStore.LoadFromFile("test/presentation_definition_mapping.json") mockAuth := auth.NewMockAuthenticationServices(ctrl) mockAuth.EXPECT().PresentationDefinitions().Return(peStore) - instance := New(mockAuth, nil, nil) + instance := New(mockAuth, nil, nil, nil) params := map[string]string{ "scope": "unsupported", @@ -139,7 +140,7 @@ func TestWrapper_handlePresentationRequest(t *testing.T) { assert.Nil(t, response) }) t.Run("invalid response_mode", func(t *testing.T) { - instance := New(nil, nil, nil) + instance := New(nil, nil, nil, nil) params := map[string]string{ "scope": "eOverdracht-overdrachtsbericht", "response_type": "code", diff --git a/auth/api/iam/session.go b/auth/api/iam/session.go index de6629f458..f72a4c1d56 100644 --- a/auth/api/iam/session.go +++ b/auth/api/iam/session.go @@ -19,34 +19,10 @@ package iam import ( - "github.com/google/uuid" "github.com/nuts-foundation/go-did/did" "net/url" - "sync" ) -type SessionManager struct { - sessions *sync.Map -} - -func (s *SessionManager) Create(session Session) string { - // TODO: Session expiration - // TODO: Session storage - // TODO: Session pinning and other safety measures (see OAuth2 Threat Model) - id := uuid.NewString() - s.sessions.Store(id, session) - return id -} - -func (s *SessionManager) Get(id string) *Session { - session, ok := s.sessions.Load(id) - if !ok { - return nil - } - result := session.(Session) - return &result -} - type Session struct { ClientID string Scope string diff --git a/cmd/root.go b/cmd/root.go index a01eaaa9f6..4c3c6b302e 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -214,7 +214,7 @@ func CreateSystem(shutdownCallback context.CancelFunc) *core.System { system.RegisterRoutes(statusEngine.(core.Routable)) system.RegisterRoutes(metricsEngine.(core.Routable)) system.RegisterRoutes(&authAPIv1.Wrapper{Auth: authInstance, CredentialResolver: credentialInstance}) - system.RegisterRoutes(authIAMAPI.New(authInstance, credentialInstance, vdrInstance)) + system.RegisterRoutes(authIAMAPI.New(authInstance, credentialInstance, vdrInstance, storageInstance)) system.RegisterRoutes(&authMeansAPI.Wrapper{Auth: authInstance}) system.RegisterRoutes(&didmanAPI.Wrapper{Didman: didmanInstance}) From e261b7aeb9d08da9278059a558ee8a4594070352 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Oct 2023 14:31:26 +0200 Subject: [PATCH 04/23] Bump golang from 1.21.1-alpine to 1.21.2-alpine (#2530) Bumps golang from 1.21.1-alpine to 1.21.2-alpine. --- updated-dependencies: - dependency-name: golang 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> --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 8c0e1df897..6fbbeb0c8f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # golang alpine -FROM golang:1.21.1-alpine as builder +FROM golang:1.21.2-alpine as builder ARG TARGETARCH ARG TARGETOS From a31236bc355057bbb8edc95b7216539fb1693766 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Oct 2023 16:07:43 +0200 Subject: [PATCH 05/23] Bump golang.org/x/crypto from 0.13.0 to 0.14.0 (#2531) Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.13.0 to 0.14.0. - [Commits](https://github.com/golang/crypto/compare/v0.13.0...v0.14.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 91a949dbaa..3742520265 100644 --- a/go.mod +++ b/go.mod @@ -41,7 +41,7 @@ require ( go.uber.org/atomic v1.11.0 go.uber.org/goleak v1.2.1 go.uber.org/mock v0.3.0 - golang.org/x/crypto v0.13.0 + golang.org/x/crypto v0.14.0 golang.org/x/time v0.3.0 google.golang.org/grpc v1.58.2 google.golang.org/protobuf v1.31.0 @@ -154,8 +154,8 @@ require ( github.com/yuin/gopher-lua v1.1.0 // indirect golang.org/x/net v0.14.0 // indirect golang.org/x/sync v0.3.0 // indirect - golang.org/x/sys v0.12.0 // indirect - golang.org/x/term v0.12.0 // indirect + golang.org/x/sys v0.13.0 // indirect + golang.org/x/term v0.13.0 // indirect golang.org/x/text v0.13.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect gopkg.in/Regis24GmbH/go-diacritics.v2 v2.0.3 // indirect diff --git a/go.sum b/go.sum index 2d86d77e34..e1217475fa 100644 --- a/go.sum +++ b/go.sum @@ -623,8 +623,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= -golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= @@ -738,14 +738,14 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU= -golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= From c0c9d23b7e16a501def447a24dcee1f4e1951457 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Oct 2023 09:19:28 +0200 Subject: [PATCH 06/23] Bump github.com/nats-io/nats-server/v2 from 2.10.1 to 2.10.2 (#2532) Bumps [github.com/nats-io/nats-server/v2](https://github.com/nats-io/nats-server) from 2.10.1 to 2.10.2. - [Release notes](https://github.com/nats-io/nats-server/releases) - [Changelog](https://github.com/nats-io/nats-server/blob/main/.goreleaser.yml) - [Commits](https://github.com/nats-io/nats-server/compare/v2.10.1...v2.10.2) --- updated-dependencies: - dependency-name: github.com/nats-io/nats-server/v2 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> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 3742520265..988835cbc2 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/lestrrat-go/jwx v1.2.26 github.com/magiconair/properties v1.8.7 github.com/mdp/qrterminal/v3 v3.1.1 - github.com/nats-io/nats-server/v2 v2.10.1 + github.com/nats-io/nats-server/v2 v2.10.2 github.com/nats-io/nats.go v1.30.2 github.com/nuts-foundation/crypto-ecies v0.0.0-20211207143025-5b84f9efce2b github.com/nuts-foundation/go-did v0.6.5 diff --git a/go.sum b/go.sum index e1217475fa..e6b443b5cc 100644 --- a/go.sum +++ b/go.sum @@ -422,8 +422,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nats-io/jwt/v2 v2.5.2 h1:DhGH+nKt+wIkDxM6qnVSKjokq5t59AZV5HRcFW0zJwU= github.com/nats-io/jwt/v2 v2.5.2/go.mod h1:24BeQtRwxRV8ruvC4CojXlx/WQ/VjuwlYiH+vu/+ibI= -github.com/nats-io/nats-server/v2 v2.10.1 h1:MIJ614dhOIdo71iSzY8ln78miXwrYvlvXHUyS+XdKZQ= -github.com/nats-io/nats-server/v2 v2.10.1/go.mod h1:3PMvMSu2cuK0J9YInRLWdFpFsswKKGUS77zVSAudRto= +github.com/nats-io/nats-server/v2 v2.10.2 h1:2o/OOyc/dxeMCQtrF1V/9er0SU0A3LKhDlv/+rqreBM= +github.com/nats-io/nats-server/v2 v2.10.2/go.mod h1:lzrskZ/4gyMAh+/66cCd+q74c6v7muBypzfWhP/MAaM= github.com/nats-io/nats.go v1.30.2 h1:aloM0TGpPorZKQhbAkdCzYDj+ZmsJDyeo3Gkbr72NuY= github.com/nats-io/nats.go v1.30.2/go.mod h1:dcfhUgmQNN4GJEfIb2f9R7Fow+gzBF4emzDHrVBd5qM= github.com/nats-io/nkeys v0.4.5 h1:Zdz2BUlFm4fJlierwvGK+yl20IAKUm7eV6AAZXEhkPk= From e125c0630aeffed47b135ad485aeeded1babde49 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Oct 2023 09:00:25 +0200 Subject: [PATCH 07/23] Bump golang from 1.21.2-alpine to 1.21.3-alpine (#2538) Bumps golang from 1.21.2-alpine to 1.21.3-alpine. --- updated-dependencies: - dependency-name: golang 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> --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 6fbbeb0c8f..a2cc2894ef 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # golang alpine -FROM golang:1.21.2-alpine as builder +FROM golang:1.21.3-alpine as builder ARG TARGETARCH ARG TARGETOS From 23dde2574b00c25f53ba368bf7f48451c7637ad7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 13 Oct 2023 11:23:06 +0200 Subject: [PATCH 08/23] Bump github.com/labstack/echo/v4 from 4.11.1 to 4.11.2 (#2537) Bumps [github.com/labstack/echo/v4](https://github.com/labstack/echo) from 4.11.1 to 4.11.2. - [Release notes](https://github.com/labstack/echo/releases) - [Changelog](https://github.com/labstack/echo/blob/master/CHANGELOG.md) - [Commits](https://github.com/labstack/echo/compare/v4.11.1...v4.11.2) --- updated-dependencies: - dependency-name: github.com/labstack/echo/v4 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> --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 988835cbc2..851a34fe33 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/google/uuid v1.3.1 github.com/hashicorp/vault/api v1.10.0 github.com/knadh/koanf v1.5.0 - github.com/labstack/echo/v4 v4.11.1 + github.com/labstack/echo/v4 v4.11.2 github.com/lestrrat-go/jwx v1.2.26 github.com/magiconair/properties v1.8.7 github.com/mdp/qrterminal/v3 v3.1.1 @@ -152,7 +152,7 @@ require ( github.com/x-cray/logrus-prefixed-formatter v0.5.2 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/yuin/gopher-lua v1.1.0 // indirect - golang.org/x/net v0.14.0 // indirect + golang.org/x/net v0.17.0 // indirect golang.org/x/sync v0.3.0 // indirect golang.org/x/sys v0.13.0 // indirect golang.org/x/term v0.13.0 // indirect diff --git a/go.sum b/go.sum index e6b443b5cc..15658e7990 100644 --- a/go.sum +++ b/go.sum @@ -331,8 +331,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/labstack/echo/v4 v4.11.1 h1:dEpLU2FLg4UVmvCGPuk/APjlH6GDpbEPti61srUUUs4= -github.com/labstack/echo/v4 v4.11.1/go.mod h1:YuYRTSM3CHs2ybfrL8Px48bO6BAnYIN4l8wSTMP6BDQ= +github.com/labstack/echo/v4 v4.11.2 h1:T+cTLQxWCDfqDEoydYm5kCobjmHwOwcv4OJAPHilmdE= +github.com/labstack/echo/v4 v4.11.2/go.mod h1:UcGuQ8V6ZNRmSweBIJkPvGfwCMIlFmiqrPqiEBfPYws= github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo= @@ -665,8 +665,8 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= -golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= From 8cfe02c83509d3eb6aee40d1b2dc022c4e31cea6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 13 Oct 2023 11:24:02 +0200 Subject: [PATCH 09/23] Bump github.com/chromedp/chromedp from 0.9.2 to 0.9.3 (#2535) Bumps [github.com/chromedp/chromedp](https://github.com/chromedp/chromedp) from 0.9.2 to 0.9.3. - [Release notes](https://github.com/chromedp/chromedp/releases) - [Commits](https://github.com/chromedp/chromedp/compare/v0.9.2...v0.9.3) --- updated-dependencies: - dependency-name: github.com/chromedp/chromedp 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> --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 851a34fe33..7a5b41c10b 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/alicebob/miniredis/v2 v2.30.5 github.com/avast/retry-go/v4 v4.5.0 github.com/cbroglie/mustache v1.4.0 - github.com/chromedp/chromedp v0.9.2 + github.com/chromedp/chromedp v0.9.3 github.com/dlclark/regexp2 v1.10.0 github.com/golang-jwt/jwt v3.2.2+incompatible github.com/goodsign/monday v1.0.1 @@ -65,7 +65,7 @@ require ( github.com/cenkalti/backoff/v3 v3.0.0 // indirect github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89 // indirect + github.com/chromedp/cdproto v0.0.0-20231011050154-1d073bb38998 // indirect github.com/chromedp/sysutil v1.0.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect @@ -85,7 +85,7 @@ require ( github.com/go-sql-driver/mysql v1.6.0 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect - github.com/gobwas/ws v1.2.1 // indirect + github.com/gobwas/ws v1.3.0 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/golang-jwt/jwt/v4 v4.4.1 // indirect github.com/golang/protobuf v1.5.3 // indirect diff --git a/go.sum b/go.sum index 15658e7990..4e2bbb8d64 100644 --- a/go.sum +++ b/go.sum @@ -77,10 +77,10 @@ github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89 h1:aPflPkRFkVwbW6dmcVqfgwp1i+UWGFH6VgR1Jim5Ygc= -github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= -github.com/chromedp/chromedp v0.9.2 h1:dKtNz4kApb06KuSXoTQIyUC2TrA0fhGDwNZf3bcgfKw= -github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs= +github.com/chromedp/cdproto v0.0.0-20231011050154-1d073bb38998 h1:2zipcnjfFdqAjOQa8otCCh0Lk1M7RBzciy3s80YAKHk= +github.com/chromedp/cdproto v0.0.0-20231011050154-1d073bb38998/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= +github.com/chromedp/chromedp v0.9.3 h1:Wq58e0dZOdHsxaj9Owmfcf+ibtpYN1N0FWVbaxa/esg= +github.com/chromedp/chromedp v0.9.3/go.mod h1:NipeUkUcuzIdFbBP8eNNvl9upcceOfWzoJn6cRe4ksA= github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic= github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -173,8 +173,8 @@ github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= 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.2.1 h1:F2aeBZrm2NDsc7vbovKrWSogd4wvfAxg0FQ89/iqOTk= -github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= +github.com/gobwas/ws v1.3.0 h1:sbeU3Y4Qzlb+MOzIe6mQGf7QR4Hkv6ZD0qhGkBFL2O0= +github.com/gobwas/ws v1.3.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= From ae5277048eb839228ec169d1100a5d0b1c21d0be Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 13 Oct 2023 11:24:29 +0200 Subject: [PATCH 10/23] Bump github.com/alicebob/miniredis/v2 from 2.30.5 to 2.31.0 (#2536) Bumps [github.com/alicebob/miniredis/v2](https://github.com/alicebob/miniredis) from 2.30.5 to 2.31.0. - [Release notes](https://github.com/alicebob/miniredis/releases) - [Changelog](https://github.com/alicebob/miniredis/blob/master/CHANGELOG.md) - [Commits](https://github.com/alicebob/miniredis/compare/v2.30.5...v2.31.0) --- updated-dependencies: - dependency-name: github.com/alicebob/miniredis/v2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 7a5b41c10b..1ffee72031 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.21 require ( github.com/PaesslerAG/jsonpath v0.1.2-0.20230323094847-3484786d6f97 - github.com/alicebob/miniredis/v2 v2.30.5 + github.com/alicebob/miniredis/v2 v2.31.0 github.com/avast/retry-go/v4 v4.5.0 github.com/cbroglie/mustache v1.4.0 github.com/chromedp/chromedp v0.9.3 diff --git a/go.sum b/go.sum index 4e2bbb8d64..051b959a35 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,7 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/DmitriyVTitov/size v1.5.0/go.mod h1:le6rNI4CoLQV1b9gzp1+3d7hMAD/uu2QcJ+aYbNgiU0= github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/PaesslerAG/gval v1.2.2 h1:Y7iBzhgE09IGTt5QgGQ2IdaYYYOU134YGHBThD+wm9E= @@ -19,8 +20,8 @@ github.com/alexandrevicenzi/go-sse v1.6.0 h1:3KvOzpuY7UrbqZgAtOEmub9/V5ykr7Myudw github.com/alexandrevicenzi/go-sse v1.6.0/go.mod h1:jdrNAhMgVqP7OfcUuM8eJx0sOY17wc+girs5utpFZUU= github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk= github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= -github.com/alicebob/miniredis/v2 v2.30.5 h1:3r6kTHdKnuP4fkS8k2IrvSfxpxUTcW1SOL0wN7b7Dt0= -github.com/alicebob/miniredis/v2 v2.30.5/go.mod h1:b25qWj4fCEsBeAAR2mlb0ufImGC6uH3VlUfb/HS5zKg= +github.com/alicebob/miniredis/v2 v2.31.0 h1:ObEFUNlJwoIiyjxdrYF0QIDE7qXcLc7D3WpSH4c22PU= +github.com/alicebob/miniredis/v2 v2.31.0/go.mod h1:UB/T2Uztp7MlFSDakaX1sTXUv5CASoprx0wulRT6HBg= github.com/alvaroloes/enumer v1.1.2/go.mod h1:FxrjvuXoDAx9isTJrv4c+T410zFi0DtXIT0m65DJ+Wo= github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= @@ -190,8 +191,8 @@ github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2V github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE= github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 h1:ZgQEtGgCBiWRM39fZuwSd1LwSqqSW0hOdXCYYDX0R3I= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= From a32e8cbb18c913081184f1da36e58daf26f9f226 Mon Sep 17 00:00:00 2001 From: reinkrul Date: Tue, 17 Oct 2023 09:39:09 +0200 Subject: [PATCH 11/23] Resolving did:key DIDs (#2523) --- go.mod | 1 + go.sum | 2 + vdr/didkey/resolver.go | 144 +++++++++++++++++++ vdr/didkey/resolver_test.go | 279 ++++++++++++++++++++++++++++++++++++ vdr/vdr.go | 2 + vdr/vdr_test.go | 11 ++ 6 files changed, 439 insertions(+) create mode 100644 vdr/didkey/resolver.go create mode 100644 vdr/didkey/resolver_test.go diff --git a/go.mod b/go.mod index 1ffee72031..33bc3ed0f7 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/lestrrat-go/jwx v1.2.26 github.com/magiconair/properties v1.8.7 github.com/mdp/qrterminal/v3 v3.1.1 + github.com/multiformats/go-multicodec v0.9.0 github.com/nats-io/nats-server/v2 v2.10.2 github.com/nats-io/nats.go v1.30.2 github.com/nuts-foundation/crypto-ecies v0.0.0-20211207143025-5b84f9efce2b diff --git a/go.sum b/go.sum index 051b959a35..bd88053e33 100644 --- a/go.sum +++ b/go.sum @@ -417,6 +417,8 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mr-tron/base58 v1.1.3 h1:v+sk57XuaCKGXpWtVBX8YJzO7hMGx4Aajh4TQbdEFdc= github.com/mr-tron/base58 v1.1.3/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/multiformats/go-multicodec v0.9.0 h1:pb/dlPnzee/Sxv/j4PmkDRxCOi3hXTz3IbPKOXWJkmg= +github.com/multiformats/go-multicodec v0.9.0/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k= github.com/multiformats/go-multihash v0.0.11 h1:yEyBxwoR/7vBM5NfLVXRnpQNVLrMhpS6MRb7Z/1pnzc= github.com/multiformats/go-multihash v0.0.11/go.mod h1:LXRDJcYYY+9BjlsFe6i5LV7uekf0OoEJdnRmitUshxk= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= diff --git a/vdr/didkey/resolver.go b/vdr/didkey/resolver.go new file mode 100644 index 0000000000..2b3fc40478 --- /dev/null +++ b/vdr/didkey/resolver.go @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2023 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 didkey + +import ( + "bytes" + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/x509" + "encoding/binary" + "errors" + "fmt" + "github.com/lestrrat-go/jwx/x25519" + "github.com/multiformats/go-multicodec" + ssi "github.com/nuts-foundation/go-did" + "github.com/nuts-foundation/go-did/did" + "github.com/nuts-foundation/nuts-node/vdr/resolver" + "github.com/shengdoushi/base58" + "io" +) + +// MethodName is the name of this DID method. +const MethodName = "key" + +var _ resolver.DIDResolver = &Resolver{} + +var errInvalidPublicKeyLength = errors.New("invalid did:key: invalid public key length") + +// NewResolver creates a new Resolver. +func NewResolver() *Resolver { + return &Resolver{} +} + +type Resolver struct { +} + +func (r Resolver) Resolve(id did.DID, _ *resolver.ResolveMetadata) (*did.Document, *resolver.DocumentMetadata, error) { + if id.Method != MethodName { + return nil, nil, fmt.Errorf("unsupported DID method: %s", id.Method) + } + encodedKey := id.ID + if len(encodedKey) == 0 || encodedKey[0] != 'z' { + return nil, nil, errors.New("did:key does not start with 'z'") + } + mcBytes, err := base58.Decode(encodedKey[1:], base58.BitcoinAlphabet) + if err != nil { + return nil, nil, fmt.Errorf("did:key: invalid base58btc: %w", err) + } + reader := bytes.NewReader(mcBytes) + keyType, err := binary.ReadUvarint(reader) + if err != nil { + return nil, nil, fmt.Errorf("did:key: invalid multicodec value: %w", err) + } + // See https://w3c-ccg.github.io/did-method-key/#signature-method-creation-algorithm + var key crypto.PublicKey + mcBytes, _ = io.ReadAll(reader) + keyLength := len(mcBytes) + + switch multicodec.Code(keyType) { + case multicodec.Bls12_381G2Pub: + return nil, nil, errors.New("did:key: bls12381 public keys are not supported") + case multicodec.X25519Pub: + if keyLength != 32 { + return nil, nil, errInvalidPublicKeyLength + } + key = x25519.PublicKey(mcBytes) + case multicodec.Ed25519Pub: + if keyLength != 32 { + return nil, nil, errInvalidPublicKeyLength + } + key = ed25519.PublicKey(mcBytes) + case multicodec.Secp256k1Pub: + // lestrrat/jwk.New() is missing support for secp256k1 + return nil, nil, errors.New("did:key: secp256k1 public keys are not supported") + case multicodec.P256Pub: + if key, err = unmarshalEC(elliptic.P256(), 33, mcBytes); err != nil { + return nil, nil, err + } + case multicodec.P384Pub: + if key, err = unmarshalEC(elliptic.P384(), 49, mcBytes); err != nil { + return nil, nil, err + } + case multicodec.P521Pub: + key, _ = unmarshalEC(elliptic.P521(), -1, mcBytes) + case multicodec.RsaPub: + rsaKey, err := x509.ParsePKCS1PublicKey(mcBytes) + if err != nil { + return nil, nil, fmt.Errorf("did:key: invalid PKCS#1 encoded RSA public key: %w", err) + } + // Safe RSA keys must be at least 2048 bits + if rsaKey.Size() < 256 { + return nil, nil, errors.New("did:key: RSA public key is too small (must be at least 2048 bits)") + } + key = rsaKey + default: + return nil, nil, fmt.Errorf("did:key: unsupported public key type: 0x%x", keyType) + } + + document := did.Document{ + Context: []ssi.URI{ + ssi.MustParseURI("https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json"), + did.DIDContextV1URI(), + }, + ID: id, + } + keyID := id + keyID.Fragment = id.ID + vm, err := did.NewVerificationMethod(keyID, ssi.JsonWebKey2020, id, key) + if err != nil { + return nil, nil, err + } + document.AddAssertionMethod(vm) + document.AddAuthenticationMethod(vm) + document.AddKeyAgreement(vm) + document.AddCapabilityDelegation(vm) + document.AddCapabilityInvocation(vm) + return &document, &resolver.DocumentMetadata{}, nil +} + +func unmarshalEC(curve elliptic.Curve, expectedLen int, pubKeyBytes []byte) (ecdsa.PublicKey, error) { + if expectedLen != -1 && len(pubKeyBytes) != expectedLen { + return ecdsa.PublicKey{}, errInvalidPublicKeyLength + } + x, y := elliptic.UnmarshalCompressed(curve, pubKeyBytes) + return ecdsa.PublicKey{Curve: curve, X: x, Y: y}, nil +} diff --git a/vdr/didkey/resolver_test.go b/vdr/didkey/resolver_test.go new file mode 100644 index 0000000000..8168569631 --- /dev/null +++ b/vdr/didkey/resolver_test.go @@ -0,0 +1,279 @@ +/* + * Copyright (C) 2023 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 didkey + +import ( + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/binary" + "encoding/json" + "github.com/multiformats/go-multicodec" + "github.com/nuts-foundation/go-did/did" + "github.com/shengdoushi/base58" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" +) + +func TestTestVectors(t *testing.T) { + resolver := Resolver{} + type testCase struct { + name string + did string + jwk map[string]interface{} + error string + } + + unsafeRSAKey, _ := rsa.GenerateKey(rand.Reader, 1024) + unsafeRSAKeyBytes := x509.MarshalPKCS1PublicKey(&unsafeRSAKey.PublicKey) + + testCases := []testCase{ + // Taken from https://w3c-ccg.github.io/did-method-key/#ed25519-x25519 + { + name: "ed25519", + did: "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp", + jwk: map[string]interface{}{ + "kty": "OKP", + "crv": "Ed25519", + "x": "O2onvM62pC1io6jQKm8Nc2UyFXcd4kOmOsBIoYtZ2ik", + }, + }, + { + name: "ed25519 (invalid length)", + did: createDIDKey(multicodec.Ed25519Pub, []byte{1, 2, 3}), + error: "invalid did:key: invalid public key length", + }, + // Taken from https://w3c-ccg.github.io/did-method-key/#x25519 + { + name: "x25519", + did: "did:key:z6LSeu9HkTHSfLLeUs2nnzUSNedgDUevfNQgQjQC23ZCit6F", + jwk: map[string]interface{}{ + "crv": "X25519", + "kty": "OKP", + "x": "L-V9o0fNYkMVKNqsX7spBzD_9oSvxM_C7ZCZX1jLO3Q", + }, + }, + { + name: "x25519 (invalid length)", + did: createDIDKey(multicodec.X25519Pub, []byte{1, 2, 3}), + error: "invalid did:key: invalid public key length", + }, + // Taken from https://w3c-ccg.github.io/did-method-key/#secp256k1 + { + name: "secp256k1", + did: "did:key:zQ3shbgnTGcgBpXPdBjDur3ATMDWhS7aPs6FRFkWR19Lb9Zwz", + error: "did:key: secp256k1 public keys are not supported", + }, + // Taken from https://w3c-ccg.github.io/did-method-key/#bls-12381 + { + name: "bls12381", + did: "did:key:zUC7K4ndUaGZgV7Cp2yJy6JtMoUHY6u7tkcSYUvPrEidqBmLCTLmi6d5WvwnUqejscAkERJ3bfjEiSYtdPkRSE8kSa11hFBr4sTgnbZ95SJj19PN2jdvJjyzpSZgxkyyxNnBNnY", + error: "did:key: bls12381 public keys are not supported", + }, + // Taken from https://w3c-ccg.github.io/did-method-key/#p-256 + { + name: "secp256", + did: "did:key:zDnaeucDGfhXHoJVqot3p21RuupNJ2fZrs8Lb1GV83VnSo2jR", + jwk: map[string]interface{}{ + "kty": "EC", + "crv": "P-256", + "x": "sYLQHOy9TNAWwFcAlpxkqRA5OutpWCrVPEWsgeli_KA", + "y": "l5Jr9_48oPJWHwuVmH_VZVquGe-U8RtnR-McN4tdYhs", + }, + }, + { + name: "secp256 (invalid length)", + did: createDIDKey(multicodec.P256Pub, []byte{1, 2, 3}), + error: "invalid did:key: invalid public key length", + }, + // Taken from https://w3c-ccg.github.io/did-method-key/#p-384 + { + name: "secp384", + did: "did:key:z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9", + jwk: map[string]interface{}{ + "kty": "EC", + "crv": "P-384", + "x": "lInTxl8fjLKp_UCrxI0WDklahi-7-_6JbtiHjiRvMvhedhKVdHBfi2HCY8t_QJyc", + "y": "y6N1IC-2mXxHreETBW7K3mBcw0qGr3CWHCs-yl09yCQRLcyfGv7XhqAngHOu51Zv", + }, + }, + { + name: "secp384 (invalid length)", + did: createDIDKey(multicodec.P384Pub, []byte{1, 2, 3}), + error: "invalid did:key: invalid public key length", + }, + // Taken from https://w3c-ccg.github.io/did-method-key/#p-521 + { + name: "secp521", + did: "did:key:z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7", + jwk: map[string]interface{}{ + "kty": "EC", + "crv": "P-521", + "x": "ASUHPMyichQ0QbHZ9ofNx_l4y7luncn5feKLo3OpJ2nSbZoC7mffolj5uy7s6KSKXFmnNWxGJ42IOrjZ47qqwqyS", + "y": "AW9ziIC4ZQQVSNmLlp59yYKrjRY0_VqO-GOIYQ9tYpPraBKUloEId6cI_vynCzlZWZtWpgOM3HPhYEgawQ703RjC", + }, + }, + // Taken from https://w3c-ccg.github.io/did-method-key/#rsa-2048 + { + name: "rsa2048", + did: "did:key:z4MXj1wBzi9jUstyPMS4jQqB6KdJaiatPkAtVtGc6bQEQEEsKTic4G7Rou3iBf9vPmT5dbkm9qsZsuVNjq8HCuW1w24nhBFGkRE4cd2Uf2tfrB3N7h4mnyPp1BF3ZttHTYv3DLUPi1zMdkULiow3M1GfXkoC6DoxDUm1jmN6GBj22SjVsr6dxezRVQc7aj9TxE7JLbMH1wh5X3kA58H3DFW8rnYMakFGbca5CB2Jf6CnGQZmL7o5uJAdTwXfy2iiiyPxXEGerMhHwhjTA1mKYobyk2CpeEcmvynADfNZ5MBvcCS7m3XkFCMNUYBS9NQ3fze6vMSUPsNa6GVYmKx2x6JrdEjCk3qRMMmyjnjCMfR4pXbRMZa3i", + jwk: map[string]interface{}{ + "kty": "RSA", + "e": "AQAB", + "n": "sbX82NTV6IylxCh7MfV4hlyvaniCajuP97GyOqSvTmoEdBOflFvZ06kR_9D6ctt45Fk6hskfnag2GG69NALVH2o4RCR6tQiLRpKcMRtDYE_thEmfBvDzm_VVkOIYfxu-Ipuo9J_S5XDNDjczx2v-3oDh5-CIHkU46hvFeCvpUS-L8TJSbgX0kjVk_m4eIb9wh63rtmD6Uz_KBtCo5mmR4TEtcLZKYdqMp3wCjN-TlgHiz_4oVXWbHUefCEe8rFnX1iQnpDHU49_SaXQoud1jCaexFn25n-Aa8f8bc5Vm-5SeRwidHa6ErvEhTvf1dz6GoNPp2iRvm-wJ1gxwWJEYPQ", + }, + }, + // Taken from https://w3c-ccg.github.io/did-method-key/#rsa-4096 + { + name: "rsa4096", + did: "did:key:zgghBUVkqmWS8e1ioRVp2WN9Vw6x4NvnE9PGAyQsPqM3fnfPf8EdauiRVfBTcVDyzhqM5FFC7ekAvuV1cJHawtfgB9wDcru1hPDobk3hqyedijhgWmsYfJCmodkiiFnjNWATE7PvqTyoCjcmrc8yMRXmFPnoASyT5beUd4YZxTE9VfgmavcPy3BSouNmASMQ8xUXeiRwjb7xBaVTiDRjkmyPD7NYZdXuS93gFhyDFr5b3XLg7Rfj9nHEqtHDa7NmAX7iwDAbMUFEfiDEf9hrqZmpAYJracAjTTR8Cvn6mnDXMLwayNG8dcsXFodxok2qksYF4D8ffUxMRmyyQVQhhhmdSi4YaMPqTnC1J6HTG9Yfb98yGSVaWi4TApUhLXFow2ZvB6vqckCNhjCRL2R4MDUSk71qzxWHgezKyDeyThJgdxydrn1osqH94oSeA346eipkJvKqYREXBKwgB5VL6WF4qAK6sVZxJp2dQBfCPVZ4EbsBQaJXaVK7cNcWG8tZBFWZ79gG9Cu6C4u8yjBS8Ux6dCcJPUTLtixQu4z2n5dCsVSNdnP1EEs8ZerZo5pBgc68w4Yuf9KL3xVxPnAB1nRCBfs9cMU6oL1EdyHbqrTfnjE8HpY164akBqe92LFVsk8RusaGsVPrMekT8emTq5y8v8CabuZg5rDs3f9NPEtogjyx49wiub1FecM5B7QqEcZSYiKHgF4mfkteT2", + jwk: map[string]interface{}{ + "kty": "RSA", + "e": "AQAB", + "n": "qMCkFFRFWtzUyZeK8mgJdyM6SEQcXC5E6JwCRVDld-jlJs8sXNOE_vliexq34wZRQ4hk53-JPFlvZ_QjRgIxdUxSMiZ3S5hlNVvvRaue6SMakA9ugQhnfXaWORro0UbPuHLms-bg5StDP8-8tIezu9c1H1FjwPcdbV6rAvKhyhnsM10qP3v2CPbdE0q3FOsihoKuTelImtO110E7N6fLn4U3EYbC4OyViqlrP1o_1M-R-tiM1cb4pD7XKJnIs6ryZdfOQSPBJwjNqSdN6Py_tdrFgPDTyacSSdpTVADOM2IMAoYbhV1N5APhnjOHBRFyKkF1HffQKpmXQLBqvUNNjuhmpVKWBtrTdcCKrglFXiw0cKGHKxIirjmiOlB_HYHg5UdosyE3_1Txct2U7-WBB6QXak1UgxCzgKYBDI8UPA0RlkUuHHP_Zg0fVXrXIInHO04MYxUeSps5qqyP6dJBu_v_BDn3zUq6LYFwJ_-xsU7zbrKYB4jaRlHPoCj_eDC-rSA2uQ4KXHBB8_aAqNFC9ukWxc26Ifz9dF968DLuL30bi-ZAa2oUh492Pw1bg89J7i4qTsOOfpQvGyDV7TGhKuUG3Hbumfr2w16S-_3EI2RIyd1nYsflE6ZmCkZQMG_lwDAFXaqfyGKEDouJuja4XH8r4fGWeGTrozIoniXT1HU", + }, + }, + { + name: "rsa (invalid key)", + did: createDIDKey(multicodec.RsaPub, []byte{1, 2, 3}), + error: "did:key: invalid PKCS#1 encoded RSA public key: asn1: structure error: tags don't match (16 vs {class:0 tag:1 length:2 isCompound:false}) {optional:false explicit:false application:false private:false defaultValue: tag: stringType:0 timeType:0 set:false omitEmpty:false} pkcs1PublicKey @2", + }, + { + name: "rsa (key too small)", + did: createDIDKey(multicodec.RsaPub, unsafeRSAKeyBytes), + error: "did:key: RSA public key is too small (must be at least 2048 bits)", + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + doc, md, err := resolver.Resolve(did.MustParseDID(tc.did), nil) + if tc.error != "" { + require.EqualError(t, err, tc.error) + return + } + require.NoError(t, err) + require.NotNil(t, doc) + require.NotNil(t, md) + // Assert getting the public key + vm := doc.VerificationMethod[0] + publicKey, err := vm.PublicKey() + require.NoError(t, err, "failed to get public key") + require.NotNil(t, publicKey, "public key is nil") + // Assert JWK type + jwk, err := vm.JWK() + + require.NoError(t, err, "failed to get JWK") + jwkJSON, _ := json.Marshal(jwk) + var jwkAsMap map[string]interface{} + _ = json.Unmarshal(jwkJSON, &jwkAsMap) + assert.Equal(t, tc.jwk, jwkAsMap) + }) + } +} + +func TestResolver_Resolve(t *testing.T) { + t.Run("did:key ID does not start with 'z' (invalid multibase encoding)", func(t *testing.T) { + _, _, err := Resolver{}.Resolve(did.MustParseDID("did:key:foo"), nil) + require.EqualError(t, err, "did:key does not start with 'z'") + }) + t.Run("did:key ID is not valid base58btc encoded 'z'", func(t *testing.T) { + _, _, err := Resolver{}.Resolve(did.MustParseDID("did:key:z291830129"), nil) + require.EqualError(t, err, "did:key: invalid base58btc: invalid base58 string") + }) + t.Run("invalid multicodec key type", func(t *testing.T) { + _, _, err := Resolver{}.Resolve(did.MustParseDID("did:key:z"), nil) + require.EqualError(t, err, "did:key: invalid multicodec value: EOF") + }) + t.Run("unsupported key type", func(t *testing.T) { + didKey := createDIDKey(multicodec.Aes256, []byte{1, 2, 3}) + _, _, err := Resolver{}.Resolve(did.MustParseDID(didKey), nil) + require.EqualError(t, err, "did:key: unsupported public key type: 0xa2") + }) + t.Run("verify created DID document", func(t *testing.T) { + const expected = ` +{ + "@context": [ + "https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json", + "https://www.w3.org/ns/did/v1" + ], + "assertionMethod": [ + "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK#z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK" + ], + "authentication": [ + "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK#z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK" + ], + "capabilityDelegation": [ + "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK#z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK" + ], + "capabilityInvocation": [ + "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK#z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK" + ], + "id": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", + "keyAgreement": [ + "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK#z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK" + ], + "verificationMethod": [ + { + "controller": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", + "id": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK#z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", + "publicKeyJwk": { + "crv": "Ed25519", + "kty": "OKP", + "x": "Lm_M42cB3HkUiODQsXRcweM6TByfzEHGO9ND274JcOY" + }, + "type": "JsonWebKey2020" + } + ] +} +` + doc, md, err := Resolver{}.Resolve(did.MustParseDID("did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"), nil) + require.NoError(t, err) + require.NotNil(t, doc) + require.NotNil(t, md) + docJSON, _ := doc.MarshalJSON() + assert.JSONEq(t, expected, string(docJSON)) + // Test the public key + publicKey, err := doc.VerificationMethod[0].PublicKey() + require.NoError(t, err) + require.IsType(t, ed25519.PublicKey{}, publicKey) + }) +} + +func TestNewResolver(t *testing.T) { + assert.NotNil(t, NewResolver()) +} + +func createDIDKey(keyType multicodec.Code, data []byte) string { + mcBytes := append(binary.AppendUvarint([]byte{}, uint64(keyType)), data...) + return "did:key:z" + string(base58.Encode(mcBytes, base58.BitcoinAlphabet)) +} + +func TestRoundTrip(t *testing.T) { + t.Run("secp384", func(t *testing.T) { + keyPair, _ := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + data := elliptic.MarshalCompressed(elliptic.P384(), keyPair.PublicKey.X, keyPair.PublicKey.Y) + key := createDIDKey(multicodec.P384Pub, data) + _, _, err := Resolver{}.Resolve(did.MustParseDID(key), nil) + require.NoError(t, err) + }) +} diff --git a/vdr/vdr.go b/vdr/vdr.go index 257f146d77..341532ac0a 100644 --- a/vdr/vdr.go +++ b/vdr/vdr.go @@ -38,6 +38,7 @@ import ( "github.com/nuts-foundation/nuts-node/network" "github.com/nuts-foundation/nuts-node/storage" "github.com/nuts-foundation/nuts-node/vdr/didjwk" + "github.com/nuts-foundation/nuts-node/vdr/didkey" "github.com/nuts-foundation/nuts-node/vdr/didnuts" didnutsStore "github.com/nuts-foundation/nuts-node/vdr/didnuts/didstore" "github.com/nuts-foundation/nuts-node/vdr/didweb" @@ -139,6 +140,7 @@ func (r *Module) Configure(_ core.ServerConfig) error { r.didResolver.Register(didnuts.MethodName, &didnuts.Resolver{Store: r.store}) r.didResolver.Register(didweb.MethodName, didweb.NewResolver()) r.didResolver.Register(didjwk.MethodName, didjwk.NewResolver()) + r.didResolver.Register(didkey.MethodName, didkey.NewResolver()) // Initiate the routines for auto-updating the data. r.networkAmbassador.Configure() diff --git a/vdr/vdr_test.go b/vdr/vdr_test.go index 30cab85a79..09c6778d67 100644 --- a/vdr/vdr_test.go +++ b/vdr/vdr_test.go @@ -555,6 +555,17 @@ func TestVDR_Configure(t *testing.T) { require.Len(t, doc.VerificationMethod, 1) assert.Equal(t, "P-256", doc.VerificationMethod[0].PublicKeyJwk["crv"]) }) + t.Run("it can resolve using did:key", func(t *testing.T) { + instance := NewVDR(nil, nil, nil, nil) + err := instance.Configure(core.ServerConfig{}) + require.NoError(t, err) + + doc, md, err := instance.Resolver().Resolve(did.MustParseDID("did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"), nil) + + assert.NoError(t, err) + assert.NotNil(t, doc) + assert.NotNil(t, md) + }) } type roundTripperFunc func(*http.Request) (*http.Response, error) From cf3c6f67197bb983d8519ba80d912bdc68129201 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Oct 2023 11:41:12 +0200 Subject: [PATCH 12/23] Bump github.com/nats-io/nats-server/v2 from 2.10.2 to 2.10.3 (#2542) Bumps [github.com/nats-io/nats-server/v2](https://github.com/nats-io/nats-server) from 2.10.2 to 2.10.3. - [Release notes](https://github.com/nats-io/nats-server/releases) - [Changelog](https://github.com/nats-io/nats-server/blob/main/.goreleaser.yml) - [Commits](https://github.com/nats-io/nats-server/compare/v2.10.2...v2.10.3) --- updated-dependencies: - dependency-name: github.com/nats-io/nats-server/v2 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> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 33bc3ed0f7..dc5702f2ed 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( github.com/magiconair/properties v1.8.7 github.com/mdp/qrterminal/v3 v3.1.1 github.com/multiformats/go-multicodec v0.9.0 - github.com/nats-io/nats-server/v2 v2.10.2 + github.com/nats-io/nats-server/v2 v2.10.3 github.com/nats-io/nats.go v1.30.2 github.com/nuts-foundation/crypto-ecies v0.0.0-20211207143025-5b84f9efce2b github.com/nuts-foundation/go-did v0.6.5 diff --git a/go.sum b/go.sum index bd88053e33..fcbac58b12 100644 --- a/go.sum +++ b/go.sum @@ -425,8 +425,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nats-io/jwt/v2 v2.5.2 h1:DhGH+nKt+wIkDxM6qnVSKjokq5t59AZV5HRcFW0zJwU= github.com/nats-io/jwt/v2 v2.5.2/go.mod h1:24BeQtRwxRV8ruvC4CojXlx/WQ/VjuwlYiH+vu/+ibI= -github.com/nats-io/nats-server/v2 v2.10.2 h1:2o/OOyc/dxeMCQtrF1V/9er0SU0A3LKhDlv/+rqreBM= -github.com/nats-io/nats-server/v2 v2.10.2/go.mod h1:lzrskZ/4gyMAh+/66cCd+q74c6v7muBypzfWhP/MAaM= +github.com/nats-io/nats-server/v2 v2.10.3 h1:nk2QVLpJUh3/AhZCJlQdTfj2oeLDvWnn1Z6XzGlNFm0= +github.com/nats-io/nats-server/v2 v2.10.3/go.mod h1:lzrskZ/4gyMAh+/66cCd+q74c6v7muBypzfWhP/MAaM= github.com/nats-io/nats.go v1.30.2 h1:aloM0TGpPorZKQhbAkdCzYDj+ZmsJDyeo3Gkbr72NuY= github.com/nats-io/nats.go v1.30.2/go.mod h1:dcfhUgmQNN4GJEfIb2f9R7Fow+gzBF4emzDHrVBd5qM= github.com/nats-io/nkeys v0.4.5 h1:Zdz2BUlFm4fJlierwvGK+yl20IAKUm7eV6AAZXEhkPk= From 5157a94139590bbedb562ef99d6baeaed5de35d7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Oct 2023 11:45:05 +0200 Subject: [PATCH 13/23] Bump google.golang.org/grpc from 1.58.2 to 1.58.3 (#2543) Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.58.2 to 1.58.3. - [Release notes](https://github.com/grpc/grpc-go/releases) - [Commits](https://github.com/grpc/grpc-go/compare/v1.58.2...v1.58.3) --- updated-dependencies: - dependency-name: google.golang.org/grpc 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> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index dc5702f2ed..0293fde3a8 100644 --- a/go.mod +++ b/go.mod @@ -44,7 +44,7 @@ require ( go.uber.org/mock v0.3.0 golang.org/x/crypto v0.14.0 golang.org/x/time v0.3.0 - google.golang.org/grpc v1.58.2 + google.golang.org/grpc v1.58.3 google.golang.org/protobuf v1.31.0 gopkg.in/Regis24GmbH/go-phonetics.v2 v2.0.3 gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum index fcbac58b12..2d3f0278bd 100644 --- a/go.sum +++ b/go.sum @@ -800,8 +800,8 @@ google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQ google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.58.2 h1:SXUpjxeVF3FKrTYQI4f4KvbGD5u2xccdYdurwowix5I= -google.golang.org/grpc v1.58.2/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= +google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ= +google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= From 83da060ea756c633322cb419892d5a1caedc1bd4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Oct 2023 11:49:35 +0200 Subject: [PATCH 14/23] Bump github.com/nats-io/nats.go from 1.30.2 to 1.31.0 (#2544) Bumps [github.com/nats-io/nats.go](https://github.com/nats-io/nats.go) from 1.30.2 to 1.31.0. - [Release notes](https://github.com/nats-io/nats.go/releases) - [Commits](https://github.com/nats-io/nats.go/compare/v1.30.2...v1.31.0) --- updated-dependencies: - dependency-name: github.com/nats-io/nats.go dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 0293fde3a8..72627d6cb7 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/mdp/qrterminal/v3 v3.1.1 github.com/multiformats/go-multicodec v0.9.0 github.com/nats-io/nats-server/v2 v2.10.3 - github.com/nats-io/nats.go v1.30.2 + github.com/nats-io/nats.go v1.31.0 github.com/nuts-foundation/crypto-ecies v0.0.0-20211207143025-5b84f9efce2b github.com/nuts-foundation/go-did v0.6.5 github.com/nuts-foundation/go-leia/v4 v4.0.0 diff --git a/go.sum b/go.sum index 2d3f0278bd..555af8eb24 100644 --- a/go.sum +++ b/go.sum @@ -427,8 +427,8 @@ github.com/nats-io/jwt/v2 v2.5.2 h1:DhGH+nKt+wIkDxM6qnVSKjokq5t59AZV5HRcFW0zJwU= github.com/nats-io/jwt/v2 v2.5.2/go.mod h1:24BeQtRwxRV8ruvC4CojXlx/WQ/VjuwlYiH+vu/+ibI= github.com/nats-io/nats-server/v2 v2.10.3 h1:nk2QVLpJUh3/AhZCJlQdTfj2oeLDvWnn1Z6XzGlNFm0= github.com/nats-io/nats-server/v2 v2.10.3/go.mod h1:lzrskZ/4gyMAh+/66cCd+q74c6v7muBypzfWhP/MAaM= -github.com/nats-io/nats.go v1.30.2 h1:aloM0TGpPorZKQhbAkdCzYDj+ZmsJDyeo3Gkbr72NuY= -github.com/nats-io/nats.go v1.30.2/go.mod h1:dcfhUgmQNN4GJEfIb2f9R7Fow+gzBF4emzDHrVBd5qM= +github.com/nats-io/nats.go v1.31.0 h1:/WFBHEc/dOKBF6qf1TZhrdEfTmOZ5JzdJ+Y3m6Y/p7E= +github.com/nats-io/nats.go v1.31.0/go.mod h1:di3Bm5MLsoB4Bx61CBTsxuarI36WbhAwOm8QrW39+i8= github.com/nats-io/nkeys v0.4.5 h1:Zdz2BUlFm4fJlierwvGK+yl20IAKUm7eV6AAZXEhkPk= github.com/nats-io/nkeys v0.4.5/go.mod h1:XUkxdLPTufzlihbamfzQ7mw/VGx6ObUs+0bN5sNvt64= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= From 0a369f74c09d98ea4f1649836d97992acc3aacf8 Mon Sep 17 00:00:00 2001 From: reinkrul Date: Fri, 20 Oct 2023 11:46:47 +0200 Subject: [PATCH 15/23] VCR: JWT support for credentials and presentations (#2520) --- auth/api/auth/v1/api_test.go | 9 +- auth/services/oauth/authz_server_test.go | 2 +- auth/services/selfsigned/signer.go | 5 +- auth/services/selfsigned/signer_test.go | 29 +- auth/services/selfsigned/validator_test.go | 10 +- crypto/jwx.go | 2 +- docs/_static/vcr/vcr_v2.yaml | 14 + docs/index.rst | 1 + .../supported-protocols-formats.rst | 40 ++ docs/pages/integrating/vc.rst | 3 + go.mod | 5 +- go.sum | 10 +- vcr/api/vcr/v2/api.go | 38 +- vcr/api/vcr/v2/api_test.go | 27 +- vcr/api/vcr/v2/generated.go | 24 ++ vcr/api/vcr/v2/types.go | 2 +- vcr/credential/validator.go | 4 +- vcr/credential/validator_test.go | 2 +- vcr/holder/interface.go | 8 + vcr/holder/wallet.go | 56 ++- vcr/holder/wallet_test.go | 171 +++++++-- vcr/issuer/interface.go | 22 +- vcr/issuer/issuer.go | 67 ++-- vcr/issuer/issuer_test.go | 212 ++++++++--- vcr/issuer/leia_store_test.go | 55 +-- vcr/issuer/mock.go | 8 +- vcr/store.go | 14 +- vcr/test/formats_integration_test.go | 167 +++++++++ vcr/test/openid4vci_integration_test.go | 16 +- vcr/verifier/verifier.go | 132 ++++++- vcr/verifier/verifier_test.go | 343 +++++++++++++----- 31 files changed, 1170 insertions(+), 328 deletions(-) create mode 100644 docs/pages/integrating/supported-protocols-formats.rst create mode 100644 vcr/test/formats_integration_test.go diff --git a/auth/api/auth/v1/api_test.go b/auth/api/auth/v1/api_test.go index 62879525f0..fd1b7a7b78 100644 --- a/auth/api/auth/v1/api_test.go +++ b/auth/api/auth/v1/api_test.go @@ -40,6 +40,7 @@ import ( "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" "net/http" + "net/http/httptest" "net/url" "reflect" "testing" @@ -165,8 +166,12 @@ func TestWrapper_GetSignSessionStatus(t *testing.T) { response, err := ctx.wrapper.GetSignSessionStatus(ctx.audit, sessionObj) - assert.Equal(t, expectedResponse, response) - assert.NoError(t, err) + require.NoError(t, err) + actualResponseJSON := httptest.NewRecorder() + require.NoError(t, response.VisitGetSignSessionStatusResponse(actualResponseJSON)) + expectedResponseJSON := httptest.NewRecorder() + require.NoError(t, expectedResponse.VisitGetSignSessionStatusResponse(expectedResponseJSON)) + assert.JSONEq(t, string(expectedResponseJSON.Body.Bytes()), string(actualResponseJSON.Body.Bytes())) }) t.Run("nok - SigningSessionStatus returns error", func(t *testing.T) { diff --git a/auth/services/oauth/authz_server_test.go b/auth/services/oauth/authz_server_test.go index 33c06ce382..a8f75548d3 100644 --- a/auth/services/oauth/authz_server_test.go +++ b/auth/services/oauth/authz_server_test.go @@ -525,7 +525,7 @@ func TestService_validateAuthorizationCredentials(t *testing.T) { err := ctx.oauthService.validateAuthorizationCredentials(tokenCtx) - assert.EqualError(t, err, "invalid jwt.vcs: cannot unmarshal authorization credential: json: cannot unmarshal string into Go value of type map[string]interface {}") + assert.EqualError(t, err, "invalid jwt.vcs: cannot unmarshal authorization credential: failed to parse token: invalid character '}' looking for beginning of value") }) t.Run("error - jwt.iss <> credentialSubject.ID mismatch", func(t *testing.T) { diff --git a/auth/services/selfsigned/signer.go b/auth/services/selfsigned/signer.go index f7d6b40f52..c4a74f2d5c 100644 --- a/auth/services/selfsigned/signer.go +++ b/auth/services/selfsigned/signer.go @@ -35,6 +35,7 @@ import ( "github.com/nuts-foundation/nuts-node/vcr" "github.com/nuts-foundation/nuts-node/vcr/credential" "github.com/nuts-foundation/nuts-node/vcr/holder" + "github.com/nuts-foundation/nuts-node/vcr/issuer" "github.com/nuts-foundation/nuts-node/vcr/signature/proof" "net/url" "time" @@ -119,7 +120,7 @@ func (v *signer) createVP(ctx context.Context, s types.Session, issuanceDate tim } expirationData := issuanceDate.Add(24 * time.Hour) - credentialOptions := vc.VerifiableCredential{ + template := vc.VerifiableCredential{ Context: []ssi.URI{credential.NutsV1ContextURI}, Type: []ssi.URI{ssi.MustParseURI(credentialType)}, Issuer: issuerID.URI(), @@ -127,7 +128,7 @@ func (v *signer) createVP(ctx context.Context, s types.Session, issuanceDate tim ExpirationDate: &expirationData, CredentialSubject: s.CredentialSubject(), } - verifiableCredential, err := v.vcr.Issuer().Issue(ctx, credentialOptions, false, false) + verifiableCredential, err := v.vcr.Issuer().Issue(ctx, template, issuer.CredentialOptions{}) if err != nil { return nil, fmt.Errorf("issue VC failed: %w", err) } diff --git a/auth/services/selfsigned/signer_test.go b/auth/services/selfsigned/signer_test.go index 11f41fecae..38221ddef2 100644 --- a/auth/services/selfsigned/signer_test.go +++ b/auth/services/selfsigned/signer_test.go @@ -132,7 +132,10 @@ func TestSessionStore_SigningSessionStatus(t *testing.T) { t.Run("status completed returns VP on SigningSessionResult", func(t *testing.T) { mockContext := newMockContext(t) ss := NewSigner(mockContext.vcr, "").(*signer) - mockContext.issuer.EXPECT().Issue(context.TODO(), gomock.Any(), false, false).Return(&testVC, nil) + mockContext.issuer.EXPECT().Issue(context.TODO(), gomock.Any(), issuer.CredentialOptions{ + Publish: false, + Public: false, + }).Return(&testVC, nil) mockContext.wallet.EXPECT().BuildPresentation(context.TODO(), gomock.Len(1), gomock.Any(), &employer, true).Return(&testVP, nil) sp, err := ss.StartSigningSession(contract.Contract{RawContractText: testContract}, params) @@ -191,16 +194,10 @@ func TestSessionStore_SigningSessionStatus(t *testing.T) { t.Run("correct VC options are passed to issuer", func(t *testing.T) { mockContext := newMockContext(t) ss := NewSigner(mockContext.vcr, "").(*signer) - mockContext.issuer.EXPECT().Issue(context.TODO(), gomock.Any(), false, false).DoAndReturn( - func(arg0 interface{}, unsignedCredential interface{}, public interface{}, publish interface{}) (*vc.VerifiableCredential, error) { - isPublic, ok := public.(bool) - isPublished, ok2 := publish.(bool) - credential, ok3 := unsignedCredential.(vc.VerifiableCredential) - require.True(t, ok) - require.True(t, ok2) - require.True(t, ok3) - assert.False(t, isPublic) - assert.False(t, isPublished) + mockContext.issuer.EXPECT().Issue(context.TODO(), gomock.Any(), issuer.CredentialOptions{}).DoAndReturn( + func(arg0 interface{}, credential vc.VerifiableCredential, options issuer.CredentialOptions) (*vc.VerifiableCredential, error) { + assert.False(t, options.Public) + assert.False(t, options.Publish) assert.Equal(t, employer.URI(), credential.Issuer) assert.Equal(t, []ssi.URI{ssi.MustParseURI("NutsEmployeeCredential")}, credential.Type) @@ -241,7 +238,10 @@ func TestSessionStore_SigningSessionStatus(t *testing.T) { t.Run("error on VC issuance", func(t *testing.T) { mockContext := newMockContext(t) ss := NewSigner(mockContext.vcr, "").(*signer) - mockContext.issuer.EXPECT().Issue(context.TODO(), gomock.Any(), false, false).Return(nil, errors.New("error")) + mockContext.issuer.EXPECT().Issue(context.TODO(), gomock.Any(), issuer.CredentialOptions{ + Publish: false, + Public: false, + }).Return(nil, errors.New("error")) sp, err := ss.StartSigningSession(contract.Contract{RawContractText: testContract}, params) require.NoError(t, err) @@ -256,7 +256,10 @@ func TestSessionStore_SigningSessionStatus(t *testing.T) { t.Run("error on building VP", func(t *testing.T) { mockContext := newMockContext(t) ss := NewSigner(mockContext.vcr, "").(*signer) - mockContext.issuer.EXPECT().Issue(context.TODO(), gomock.Any(), false, false).Return(&testVC, nil) + mockContext.issuer.EXPECT().Issue(context.TODO(), gomock.Any(), issuer.CredentialOptions{ + Publish: false, + Public: false, + }).Return(&testVC, nil) mockContext.wallet.EXPECT().BuildPresentation(context.TODO(), gomock.Len(1), gomock.Any(), &employer, true).Return(nil, errors.New("error")) sp, err := ss.StartSigningSession(contract.Contract{RawContractText: testContract}, params) diff --git a/auth/services/selfsigned/validator_test.go b/auth/services/selfsigned/validator_test.go index 046c23c94e..54c5ee8899 100644 --- a/auth/services/selfsigned/validator_test.go +++ b/auth/services/selfsigned/validator_test.go @@ -91,7 +91,10 @@ func TestSigner_Validator_Roundtrip(t *testing.T) { // #2428: NutsEmployeeCredential does not need to be trusted, but the issuer needs to have a trusted NutsOrganizationCredential (chain of trust). // Issue() automatically trusts the issuer, so untrust it for asserting trust chain behavior - nutsOrgCred, err := vcrContext.VCR.Issuer().Issue(audit.TestContext(), createOrganizationCredential(issuerDID), false, false) + nutsOrgCred, err := vcrContext.VCR.Issuer().Issue(audit.TestContext(), createOrganizationCredential(issuerDID), issuer.CredentialOptions{ + Publish: false, + Public: false, + }) require.NoError(t, err) err = vcrContext.VCR.StoreCredential(*nutsOrgCred, nil) // Need to explicitly store, since we didn't publish it. require.NoError(t, err) @@ -203,7 +206,10 @@ func TestValidator_VerifyVP(t *testing.T) { // Otherwise, the NutsOrganizationCredential is not yet valid or might be expired. return vpValidTime.Add(-1 * time.Hour) } - nutsOrgCred, err := vcrContext.VCR.Issuer().Issue(audit.TestContext(), createOrganizationCredential(didDocument.ID.String()), false, false) + nutsOrgCred, err := vcrContext.VCR.Issuer().Issue(audit.TestContext(), createOrganizationCredential(didDocument.ID.String()), issuer.CredentialOptions{ + Publish: false, + Public: false, + }) require.NoError(t, err) err = vcrContext.VCR.StoreCredential(*nutsOrgCred, &vpValidTime) // Need to explicitly store, since we didn't publish it. require.NoError(t, err) diff --git a/crypto/jwx.go b/crypto/jwx.go index 25c015cf4a..0d9c0655e4 100644 --- a/crypto/jwx.go +++ b/crypto/jwx.go @@ -41,7 +41,7 @@ import ( // 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.PS256, jwa.PS384, jwa.PS512, jwa.ES256, jwa.ES384, jwa.ES512} +var supportedAlgorithms = []jwa.SignatureAlgorithm{jwa.PS256, jwa.PS384, jwa.PS512, jwa.ES256, jwa.EdDSA, jwa.ES384, jwa.ES512} const defaultRsaEncryptionAlgorithm = jwa.RSA_OAEP_256 const defaultEcEncryptionAlgorithm = jwa.ECDH_ES_A256KW diff --git a/docs/_static/vcr/vcr_v2.yaml b/docs/_static/vcr/vcr_v2.yaml index 052b134bdd..a037c1ff0b 100644 --- a/docs/_static/vcr/vcr_v2.yaml +++ b/docs/_static/vcr/vcr_v2.yaml @@ -455,6 +455,13 @@ components: description: RFC3339 time string until when the credential is valid. type: string example: "2012-01-02T12:00:00Z" + format: + description: Proof format for the credential (ldp_vc for JSON-LD or jwt_vc for JWT). If not set, it defaults to JSON-LD. + default: ldp_vc + type: string + enum: + - ldp_vc + - jwt_vc publishToNetwork: description: | If set, the node publishes this credential to the network. This is the default behaviour. @@ -587,6 +594,13 @@ components: type: string description: Date and time at which proof will expire. If omitted, the proof does not have an end date. example: '2021-12-20T09:00:00Z' + format: + description: Proof format for the presentation (JSON-LD or JWT). If not set, it defaults to JSON-LD. + default: ldp_vp + type: string + enum: + - ldp_vp + - jwt_vp VPVerificationRequest: required: diff --git a/docs/index.rst b/docs/index.rst index 3f6970f16a..131c752e84 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -19,6 +19,7 @@ Nuts documentation pages/integrating/api.rst pages/integrating/api-authentication.rst pages/integrating/vc.rst + pages/integrating/supported-protocols-formats.rst pages/integrating/faq-errors.rst pages/release_notes.rst pages/roadmap.rst diff --git a/docs/pages/integrating/supported-protocols-formats.rst b/docs/pages/integrating/supported-protocols-formats.rst new file mode 100644 index 0000000000..9389388a2f --- /dev/null +++ b/docs/pages/integrating/supported-protocols-formats.rst @@ -0,0 +1,40 @@ +.. _supported_protocols_and_formats: + +Supported Protocols and Formats +=============================== + +This page documents which cryptographic algorithms, key types and SSI formats and protocols are supported. + +Cryptographic Algorithms +************************ +The following cryptographic signing algorithms are supported: + +- ECDSA with the NIST P-256, P-384 and P-512 curves. +- EdDSA with Ed25519 curves. +- RSASSA-PSS RSA with keys of at least 2048 bits. + +The following encryption algorithms are supported: + +- RSA-OAEP-SHA256 (min. 2048 bits) +- ECDH-ES+A256KW +- AES-GCM-256 + +DID methods +*********** + +The following DID methods are supported: + +- ``did:nuts`` (creating and resolving) +- ``did:web`` (creating and resolving) +- ``did:key`` (resolving) +- ``did:jwk`` (resolving) + +Credentials +*********** + +`W3C Verifiable Credentials v1 `_ and Presentations are supported (issuing and verifying) in JSON-LD and JWT format. + +The following protocols are being implemented (work in progress): +- OpenID4VP verifier and SIOPv2 relying party for requesting a presentation from a wallet. +- OpenID4VCI issuer for issuing a credential to a wallet. +- OpenID4VCI wallet for receiving a credential from an issuer. \ No newline at end of file diff --git a/docs/pages/integrating/vc.rst b/docs/pages/integrating/vc.rst index 9c1f60b0d6..4cce4c018a 100644 --- a/docs/pages/integrating/vc.rst +++ b/docs/pages/integrating/vc.rst @@ -70,6 +70,9 @@ Will be expanded by the node to: The `visibility` property indicates the contents of the VC are published on the network, so it can be read by everyone. +By default, the node will create credentials in JSON-LD format. +You can specify the format by passing the `format` parameter (``jwt_vc`` or ``ldp_vc``). + .. _searching-vcs: Searching VCs diff --git a/go.mod b/go.mod index 72627d6cb7..e24ca76502 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/nats-io/nats-server/v2 v2.10.3 github.com/nats-io/nats.go v1.31.0 github.com/nuts-foundation/crypto-ecies v0.0.0-20211207143025-5b84f9efce2b - github.com/nuts-foundation/go-did v0.6.5 + github.com/nuts-foundation/go-did v0.7.1 github.com/nuts-foundation/go-leia/v4 v4.0.0 github.com/nuts-foundation/go-stoabs v1.9.0 // check the oapi-codegen tool version in the makefile when upgrading the runtime @@ -125,6 +125,9 @@ require ( github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/mr-tron/base58 v1.1.3 // indirect + github.com/multiformats/go-base32 v0.0.3 // indirect + github.com/multiformats/go-base36 v0.1.0 // indirect + github.com/multiformats/go-multibase v0.2.0 // indirect github.com/multiformats/go-multihash v0.0.11 // indirect github.com/nats-io/jwt/v2 v2.5.2 // indirect github.com/nats-io/nkeys v0.4.5 // indirect diff --git a/go.sum b/go.sum index 555af8eb24..921017a8c7 100644 --- a/go.sum +++ b/go.sum @@ -417,6 +417,12 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mr-tron/base58 v1.1.3 h1:v+sk57XuaCKGXpWtVBX8YJzO7hMGx4Aajh4TQbdEFdc= github.com/mr-tron/base58 v1.1.3/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/multiformats/go-base32 v0.0.3 h1:tw5+NhuwaOjJCC5Pp82QuXbrmLzWg7uxlMFp8Nq/kkI= +github.com/multiformats/go-base32 v0.0.3/go.mod h1:pLiuGC8y0QR3Ue4Zug5UzK9LjgbkL8NSQj0zQ5Nz/AA= +github.com/multiformats/go-base36 v0.1.0 h1:JR6TyF7JjGd3m6FbLU2cOxhC0Li8z8dLNGQ89tUg4F4= +github.com/multiformats/go-base36 v0.1.0/go.mod h1:kFGE83c6s80PklsHO9sRn2NCoffoRdUUOENyW/Vv6sM= +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-multicodec v0.9.0 h1:pb/dlPnzee/Sxv/j4PmkDRxCOi3hXTz3IbPKOXWJkmg= github.com/multiformats/go-multicodec v0.9.0/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k= github.com/multiformats/go-multihash v0.0.11 h1:yEyBxwoR/7vBM5NfLVXRnpQNVLrMhpS6MRb7Z/1pnzc= @@ -438,8 +444,8 @@ github.com/nightlyone/lockfile v1.0.0/go.mod h1:rywoIealpdNse2r832aiD9jRk8ErCatR github.com/npillmayer/nestext v0.1.3/go.mod h1:h2lrijH8jpicr25dFY+oAJLyzlya6jhnuG+zWp9L0Uk= 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.6.5 h1:y2gPygRN1gBeMI9y8OIWwARp8NpHHheqnbpLwCxajFw= -github.com/nuts-foundation/go-did v0.6.5/go.mod h1:Jb3IgnO2Zeed970JMIlfjr4g1kvikmgWUJA0EfeDEFE= +github.com/nuts-foundation/go-did v0.7.1 h1:JKn9QMuOq4eXHPGdYgsSk3XOxRDBWHkPPFYLFbYut0Y= +github.com/nuts-foundation/go-did v0.7.1/go.mod h1:fq65EPzzpdxD+WG5VFqMfbVaADMwbEUB4CpgPajp1LM= github.com/nuts-foundation/go-leia/v4 v4.0.0 h1:/unYCk18qGG2HWcJK4ld4CaM6k7Tdr0bR1vQd1Jwfcg= github.com/nuts-foundation/go-leia/v4 v4.0.0/go.mod h1:A246dA4nhY99OPCQpG/XbQ/iPyyfSaJchanivuPWpao= github.com/nuts-foundation/go-stoabs v1.9.0 h1:zK+ugfolaJYyBvGwsRuavLVdycXk4Yw/1gI+tz17lWQ= diff --git a/vcr/api/vcr/v2/api.go b/vcr/api/vcr/v2/api.go index a7d9204289..d77368d0d6 100644 --- a/vcr/api/vcr/v2/api.go +++ b/vcr/api/vcr/v2/api.go @@ -24,6 +24,7 @@ import ( "errors" "github.com/nuts-foundation/nuts-node/audit" "github.com/nuts-foundation/nuts-node/vcr/holder" + "github.com/nuts-foundation/nuts-node/vcr/issuer" "github.com/nuts-foundation/nuts-node/vdr/resolver" "net/http" @@ -86,27 +87,25 @@ func (w *Wrapper) ResolveStatusCode(err error) int { // IssueVC handles the API request for credential issuing. func (w Wrapper) IssueVC(ctx context.Context, request IssueVCRequestObject) (IssueVCResponseObject, error) { - var ( - publish bool - public bool - ) - - // publish is true by default + options := issuer.CredentialOptions{ + Publish: true, + } if request.Body.PublishToNetwork != nil { - publish = *request.Body.PublishToNetwork - } else { - publish = true + options.Publish = *request.Body.PublishToNetwork + } + if request.Body.Format != nil { + options.Format = string(*request.Body.Format) } // Check param constraints: if request.Body.Visibility == nil || *request.Body.Visibility == "" { - if publish { + if options.Publish { return nil, core.InvalidInputError("visibility must be set when publishing credential") } } else { // visibility is set // Visibility can only be used when publishing - if !publish { + if !options.Publish { return nil, core.InvalidInputError("visibility setting is only allowed when publishing to the network") } // Check if the values are in range @@ -114,7 +113,7 @@ func (w Wrapper) IssueVC(ctx context.Context, request IssueVCRequestObject) (Iss return nil, core.InvalidInputError("invalid value for visibility") } // Set the actual value - public = *request.Body.Visibility == Public + options.Public = *request.Body.Visibility == Public } // Set default context, if not set @@ -136,8 +135,17 @@ func (w Wrapper) IssueVC(ctx context.Context, request IssueVCRequestObject) (Iss if err := json.Unmarshal(rawRequest, &requestedVC); err != nil { return nil, err } + // Copy parsed credential to keep control over what we pass to the issuer, + // (and also makes unit testing easier since vc.VerifiableCredential has unexported fields that can't be set). + template := vc.VerifiableCredential{ + Context: requestedVC.Context, + Type: requestedVC.Type, + Issuer: requestedVC.Issuer, + ExpirationDate: requestedVC.ExpirationDate, + CredentialSubject: requestedVC.CredentialSubject, + } - vcCreated, err := w.VCR.Issuer().Issue(ctx, requestedVC, publish, public) + vcCreated, err := w.VCR.Issuer().Issue(ctx, template, options) if err != nil { return nil, err } @@ -253,6 +261,10 @@ func (w *Wrapper) CreateVP(ctx context.Context, request CreateVPRequestObject) ( presentationOptions.ProofOptions.ProofPurpose = string(purpose) } + if request.Body.Format != nil { + presentationOptions.Format = string(*request.Body.Format) + } + // pass context and type as ssi.URI if request.Body.Context != nil { for _, sc := range *request.Body.Context { diff --git a/vcr/api/vcr/v2/api_test.go b/vcr/api/vcr/v2/api_test.go index 33eb6f98b9..0a417864ed 100644 --- a/vcr/api/vcr/v2/api_test.go +++ b/vcr/api/vcr/v2/api_test.go @@ -68,7 +68,10 @@ func TestWrapper_IssueVC(t *testing.T) { Visibility: &public, } // assert that credential.NutsV1ContextURI is added if the request does not contain @context - testContext.mockIssuer.EXPECT().Issue(testContext.requestCtx, gomock.Eq(expectedRequestedVC), true, true).Return(&expectedRequestedVC, nil) + testContext.mockIssuer.EXPECT().Issue(testContext.requestCtx, expectedRequestedVC, issuer.CredentialOptions{ + Publish: true, + Public: true, + }).Return(&expectedRequestedVC, nil) response, err := testContext.client.IssueVC(testContext.requestCtx, IssueVCRequestObject{Body: &request}) @@ -100,9 +103,8 @@ func TestWrapper_IssueVC(t *testing.T) { public := Public request := IssueVCRequest{ - Type: expectedRequestedVC.Type[0].String(), - Issuer: expectedRequestedVC.Issuer.String(), - //CredentialSubject: expectedRequestedVC.CredentialSubject, + Type: expectedRequestedVC.Type[0].String(), + Issuer: expectedRequestedVC.Issuer.String(), Visibility: &public, } @@ -129,7 +131,10 @@ func TestWrapper_IssueVC(t *testing.T) { } expectedVC := vc.VerifiableCredential{} expectedResponse := IssueVC200JSONResponse(expectedVC) - testContext.mockIssuer.EXPECT().Issue(testContext.requestCtx, gomock.Any(), true, false).Return(&expectedVC, nil) + testContext.mockIssuer.EXPECT().Issue(testContext.requestCtx, gomock.Any(), issuer.CredentialOptions{ + Publish: true, + Public: false, + }).Return(&expectedVC, nil) response, err := testContext.client.IssueVC(testContext.requestCtx, IssueVCRequestObject{Body: &request}) @@ -150,7 +155,10 @@ func TestWrapper_IssueVC(t *testing.T) { } expectedVC := vc.VerifiableCredential{} expectedResponse := IssueVC200JSONResponse(expectedVC) - testContext.mockIssuer.EXPECT().Issue(testContext.requestCtx, gomock.Any(), true, true).Return(&expectedVC, nil) + testContext.mockIssuer.EXPECT().Issue(testContext.requestCtx, gomock.Any(), issuer.CredentialOptions{ + Publish: true, + Public: true, + }).Return(&expectedVC, nil) response, err := testContext.client.IssueVC(testContext.requestCtx, IssueVCRequestObject{Body: &request}) @@ -219,7 +227,10 @@ func TestWrapper_IssueVC(t *testing.T) { } expectedVC := vc.VerifiableCredential{} expectedResponse := IssueVC200JSONResponse(expectedVC) - testContext.mockIssuer.EXPECT().Issue(testContext.requestCtx, gomock.Any(), false, false).Return(&expectedVC, nil) + testContext.mockIssuer.EXPECT().Issue(testContext.requestCtx, gomock.Any(), issuer.CredentialOptions{ + Publish: false, + Public: false, + }).Return(&expectedVC, nil) response, err := testContext.client.IssueVC(testContext.requestCtx, IssueVCRequestObject{Body: &request}) @@ -267,7 +278,7 @@ func TestWrapper_IssueVC(t *testing.T) { t.Run(test.name, func(t *testing.T) { testContext := newMockContext(t) - testContext.mockIssuer.EXPECT().Issue(testContext.requestCtx, gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, test.err) + testContext.mockIssuer.EXPECT().Issue(testContext.requestCtx, gomock.Any(), gomock.Any()).Return(nil, test.err) _, err := testContext.client.IssueVC(testContext.requestCtx, IssueVCRequestObject{Body: &validIssueRequest}) diff --git a/vcr/api/vcr/v2/generated.go b/vcr/api/vcr/v2/generated.go index 82e78fd22f..1f2c316ddb 100644 --- a/vcr/api/vcr/v2/generated.go +++ b/vcr/api/vcr/v2/generated.go @@ -22,6 +22,12 @@ const ( JwtBearerAuthScopes = "jwtBearerAuth.Scopes" ) +// Defines values for CreateVPRequestFormat. +const ( + JwtVp CreateVPRequestFormat = "jwt_vp" + LdpVp CreateVPRequestFormat = "ldp_vp" +) + // Defines values for CreateVPRequestProofPurpose. const ( AssertionMethod CreateVPRequestProofPurpose = "assertionMethod" @@ -31,6 +37,12 @@ const ( KeyAgreement CreateVPRequestProofPurpose = "keyAgreement" ) +// Defines values for IssueVCRequestFormat. +const ( + JwtVc IssueVCRequestFormat = "jwt_vc" + LdpVc IssueVCRequestFormat = "ldp_vc" +) + // Defines values for IssueVCRequestVisibility. const ( Private IssueVCRequestVisibility = "private" @@ -54,6 +66,9 @@ type CreateVPRequest struct { // Expires Date and time at which proof will expire. If omitted, the proof does not have an end date. Expires *string `json:"expires,omitempty"` + // Format Proof format for the presentation (JSON-LD or JWT). If not set, it defaults to JSON-LD. + Format *CreateVPRequestFormat `json:"format,omitempty"` + // ProofPurpose The specific intent for the proof, the reason why an entity created it. Acts as a safeguard to prevent the // proof from being misused for a purpose other than the one it was intended for. ProofPurpose *CreateVPRequestProofPurpose `json:"proofPurpose,omitempty"` @@ -68,6 +83,9 @@ type CreateVPRequest struct { VerifiableCredentials []VerifiableCredential `json:"verifiableCredentials"` } +// CreateVPRequestFormat Proof format for the presentation (JSON-LD or JWT). If not set, it defaults to JSON-LD. +type CreateVPRequestFormat string + // CreateVPRequestProofPurpose The specific intent for the proof, the reason why an entity created it. Acts as a safeguard to prevent the // proof from being misused for a purpose other than the one it was intended for. type CreateVPRequestProofPurpose string @@ -93,6 +111,9 @@ type IssueVCRequest struct { // ExpirationDate RFC3339 time string until when the credential is valid. ExpirationDate *string `json:"expirationDate,omitempty"` + // Format Proof format for the credential (ldp_vc for JSON-LD or jwt_vc for JWT). If not set, it defaults to JSON-LD. + Format *IssueVCRequestFormat `json:"format,omitempty"` + // Issuer DID according to Nuts specification. Issuer string `json:"issuer"` @@ -110,6 +131,9 @@ type IssueVCRequest struct { Visibility *IssueVCRequestVisibility `json:"visibility,omitempty"` } +// IssueVCRequestFormat Proof format for the credential (ldp_vc for JSON-LD or jwt_vc for JWT). If not set, it defaults to JSON-LD. +type IssueVCRequestFormat string + // IssueVCRequestVisibility When publishToNetwork is true, the credential can be published publicly or privately to the holder. // This field is mandatory if publishToNetwork is true to prevent accidents. It defaults to "private". type IssueVCRequestVisibility string diff --git a/vcr/api/vcr/v2/types.go b/vcr/api/vcr/v2/types.go index bf0d66d2a8..6b13a861ce 100644 --- a/vcr/api/vcr/v2/types.go +++ b/vcr/api/vcr/v2/types.go @@ -41,7 +41,7 @@ var _ json.Marshaler = (*IssueVC200JSONResponse)(nil) var _ json.Marshaler = (*ResolveVC200JSONResponse)(nil) var _ json.Marshaler = (*CreateVP200JSONResponse)(nil) -// MarshalJSON forwards the call to the underlying VerifiableCredential to make sure the expected JSON-LD is returned. +// MarshalJSON forwards the call to the underlying VerifiableCredential to make sure the credential is returned in the expected format (JSON-LD or JWT). func (r IssueVC200JSONResponse) MarshalJSON() ([]byte, error) { return vc.VerifiableCredential(r).MarshalJSON() } diff --git a/vcr/credential/validator.go b/vcr/credential/validator.go index 7aa72d7266..39803f709b 100644 --- a/vcr/credential/validator.go +++ b/vcr/credential/validator.go @@ -105,8 +105,8 @@ func (d defaultCredentialValidator) Validate(credential vc.VerifiableCredential) return failure("'issuanceDate' is required") } - if credential.Proof == nil { - return failure("'proof' is required") + if credential.Format() == vc.JSONLDCredentialProofFormat && credential.Proof == nil { + return failure("'proof' is required for JSON-LD credentials") } return nil diff --git a/vcr/credential/validator_test.go b/vcr/credential/validator_test.go index 0e9644afa2..76093f1c04 100644 --- a/vcr/credential/validator_test.go +++ b/vcr/credential/validator_test.go @@ -434,7 +434,7 @@ func TestDefaultCredentialValidator(t *testing.T) { err := validator.Validate(*v) - assert.EqualError(t, err, "validation failed: 'proof' is required") + assert.EqualError(t, err, "validation failed: 'proof' is required for JSON-LD credentials") }) t.Run("failed - missing default context", func(t *testing.T) { diff --git a/vcr/holder/interface.go b/vcr/holder/interface.go index a0129daa19..0b11ba7e43 100644 --- a/vcr/holder/interface.go +++ b/vcr/holder/interface.go @@ -33,6 +33,11 @@ var VerifiableCredentialLDContextV1 = ssi.MustParseURI("https://www.w3.org/2018/ // VerifiablePresentationLDType holds the JSON-LD type for Verifiable Presentations. var VerifiablePresentationLDType = ssi.MustParseURI("VerifiablePresentation") +const ( + JSONLDPresentationFormat = vc.JSONLDPresentationProofFormat + JWTPresentationFormat = vc.JWTPresentationProofFormat +) + // Wallet holds Verifiable Credentials and can present them. type Wallet interface { core.Diagnosable @@ -62,4 +67,7 @@ type PresentationOptions struct { AdditionalTypes []ssi.URI // ProofOptions contains the options for a specific proof. ProofOptions proof.ProofOptions + // Format contains the requested format for the VerifiablePresentation. If not set, it defaults to JSON-LD. + // Valid options are: ldp_vp or jwt_vp + Format string } diff --git a/vcr/holder/wallet.go b/vcr/holder/wallet.go index 24d2657939..372603dfb0 100644 --- a/vcr/holder/wallet.go +++ b/vcr/holder/wallet.go @@ -24,6 +24,8 @@ import ( "encoding/json" "errors" "fmt" + "github.com/lestrrat-go/jwx/jws" + "github.com/lestrrat-go/jwx/jwt" ssi "github.com/nuts-foundation/go-did" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/go-did/vc" @@ -36,6 +38,7 @@ import ( "github.com/nuts-foundation/nuts-node/vcr/signature/proof" "github.com/nuts-foundation/nuts-node/vcr/verifier" "github.com/nuts-foundation/nuts-node/vdr/resolver" + "time" ) const statsShelf = "stats" @@ -90,6 +93,48 @@ func (h wallet) BuildPresentation(ctx context.Context, credentials []vc.Verifiab } } + switch options.Format { + case JWTPresentationFormat: + return h.buildJWTPresentation(ctx, *signerDID, credentials, options, key) + case "": + fallthrough + case JSONLDPresentationFormat: + return h.buildJSONLDPresentation(ctx, credentials, options, key) + default: + return nil, errors.New("unsupported presentation proof format") + } +} + +// buildJWTPresentation builds a JWT presentation according to https://www.w3.org/TR/vc-data-model/#json-web-token +func (h wallet) buildJWTPresentation(ctx context.Context, subjectDID did.DID, credentials []vc.VerifiableCredential, options PresentationOptions, key crypto.Key) (*vc.VerifiablePresentation, error) { + headers := map[string]interface{}{ + jws.TypeKey: "JWT", + } + claims := map[string]interface{}{ + jwt.IssuerKey: subjectDID.String(), + jwt.SubjectKey: subjectDID.String(), + "vp": vc.VerifiablePresentation{ + Context: append([]ssi.URI{VerifiableCredentialLDContextV1}, options.AdditionalContexts...), + Type: append([]ssi.URI{VerifiablePresentationLDType}, options.AdditionalTypes...), + VerifiableCredential: credentials, + }, + } + if options.ProofOptions.Created.IsZero() { + claims[jwt.NotBeforeKey] = time.Now().Unix() + } else { + claims[jwt.NotBeforeKey] = int(options.ProofOptions.Created.Unix()) + } + if options.ProofOptions.Expires != nil { + claims[jwt.ExpirationKey] = int(options.ProofOptions.Expires.Unix()) + } + token, err := h.keyStore.SignJWT(ctx, claims, headers, key) + if err != nil { + return nil, fmt.Errorf("unable to sign JWT presentation: %w", err) + } + return vc.ParseVerifiablePresentation(token) +} + +func (h wallet) buildJSONLDPresentation(ctx context.Context, credentials []vc.VerifiableCredential, options PresentationOptions, key crypto.Key) (*vc.VerifiablePresentation, error) { ldContext := []ssi.URI{VerifiableCredentialLDContextV1, signature.JSONWebSignature2020Context} ldContext = append(ldContext, options.AdditionalContexts...) types := []ssi.URI{VerifiablePresentationLDType} @@ -119,15 +164,8 @@ func (h wallet) BuildPresentation(ctx context.Context, credentials []vc.Verifiab if err != nil { return nil, fmt.Errorf("unable to sign VP with LD proof: %w", err) } - - var signedVP vc.VerifiablePresentation - signedVPData, _ := json.Marshal(signingResult) - err = json.Unmarshal(signedVPData, &signedVP) - if err != nil { - return nil, err - } - - return &signedVP, nil + resultJSON, _ := json.Marshal(signingResult) + return vc.ParseVerifiablePresentation(string(resultJSON)) } func (h wallet) Put(ctx context.Context, credentials ...vc.VerifiableCredential) error { diff --git a/vcr/holder/wallet_test.go b/vcr/holder/wallet_test.go index a2b9dfca02..a4c9d30dfc 100644 --- a/vcr/holder/wallet_test.go +++ b/vcr/holder/wallet_test.go @@ -58,60 +58,137 @@ func TestWallet_BuildPresentation(t *testing.T) { _ = keyStorage.SavePrivateKey(ctx, key.KID(), key.PrivateKey) keyStore := crypto.NewTestCryptoInstance(keyStorage) - options := PresentationOptions{ProofOptions: proof.ProofOptions{}} + t.Run("JSON-LD", func(t *testing.T) { + t.Run("is default", func(t *testing.T) { + ctrl := gomock.NewController(t) - t.Run("ok - one VC", func(t *testing.T) { - ctrl := gomock.NewController(t) + keyResolver := resolver.NewMockKeyResolver(ctrl) + keyResolver.EXPECT().ResolveKey(testDID, nil, resolver.NutsSigningKeyType).Return(ssi.MustParseURI(kid), key.Public(), nil) - keyResolver := resolver.NewMockKeyResolver(ctrl) - keyResolver.EXPECT().ResolveKey(testDID, nil, resolver.NutsSigningKeyType).Return(ssi.MustParseURI(kid), key.Public(), nil) + w := New(keyResolver, keyStore, nil, jsonldManager, nil) - w := New(keyResolver, keyStore, nil, jsonldManager, nil) + result, err := w.BuildPresentation(ctx, []vc.VerifiableCredential{testCredential}, PresentationOptions{}, &testDID, false) - resultingPresentation, err := w.BuildPresentation(ctx, []vc.VerifiableCredential{testCredential}, options, &testDID, false) + require.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, JSONLDPresentationFormat, result.Format()) + }) + t.Run("ok - one VC", func(t *testing.T) { + ctrl := gomock.NewController(t) - require.NoError(t, err) - assert.NotNil(t, resultingPresentation) - }) - t.Run("ok - custom options", func(t *testing.T) { - ctrl := gomock.NewController(t) - specialType := ssi.MustParseURI("SpecialPresentation") - options := PresentationOptions{ - AdditionalContexts: []ssi.URI{credential.NutsV1ContextURI}, - AdditionalTypes: []ssi.URI{specialType}, - ProofOptions: proof.ProofOptions{ - ProofPurpose: "authentication", - }, - } - keyResolver := resolver.NewMockKeyResolver(ctrl) + keyResolver := resolver.NewMockKeyResolver(ctrl) + keyResolver.EXPECT().ResolveKey(testDID, nil, resolver.NutsSigningKeyType).Return(ssi.MustParseURI(kid), key.Public(), nil) - keyResolver.EXPECT().ResolveKey(testDID, nil, resolver.NutsSigningKeyType).Return(ssi.MustParseURI(kid), key.Public(), nil) + w := New(keyResolver, keyStore, nil, jsonldManager, nil) - w := New(keyResolver, keyStore, nil, jsonldManager, nil) + result, err := w.BuildPresentation(ctx, []vc.VerifiableCredential{testCredential}, PresentationOptions{Format: JSONLDPresentationFormat}, &testDID, false) - resultingPresentation, err := w.BuildPresentation(ctx, []vc.VerifiableCredential{testCredential}, options, &testDID, false) + require.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, JSONLDPresentationFormat, result.Format()) + }) + t.Run("ok - custom options", func(t *testing.T) { + ctrl := gomock.NewController(t) + specialType := ssi.MustParseURI("SpecialPresentation") + options := PresentationOptions{ + AdditionalContexts: []ssi.URI{credential.NutsV1ContextURI}, + AdditionalTypes: []ssi.URI{specialType}, + ProofOptions: proof.ProofOptions{ + ProofPurpose: "authentication", + }, + Format: JSONLDPresentationFormat, + } + keyResolver := resolver.NewMockKeyResolver(ctrl) - require.NoError(t, err) - require.NotNil(t, resultingPresentation) - assert.True(t, resultingPresentation.IsType(specialType)) - assert.True(t, resultingPresentation.ContainsContext(credential.NutsV1ContextURI)) - proofs, _ := resultingPresentation.Proofs() - require.Len(t, proofs, 1) - assert.Equal(t, proofs[0].ProofPurpose, "authentication") + keyResolver.EXPECT().ResolveKey(testDID, nil, resolver.NutsSigningKeyType).Return(ssi.MustParseURI(kid), key.Public(), nil) + + w := New(keyResolver, keyStore, nil, jsonldManager, nil) + + result, err := w.BuildPresentation(ctx, []vc.VerifiableCredential{testCredential}, options, &testDID, false) + + require.NoError(t, err) + require.NotNil(t, result) + assert.True(t, result.IsType(specialType)) + assert.True(t, result.ContainsContext(credential.NutsV1ContextURI)) + proofs, _ := result.Proofs() + require.Len(t, proofs, 1) + assert.Equal(t, proofs[0].ProofPurpose, "authentication") + assert.Equal(t, JSONLDPresentationFormat, result.Format()) + }) + t.Run("ok - multiple VCs", func(t *testing.T) { + ctrl := gomock.NewController(t) + + keyResolver := resolver.NewMockKeyResolver(ctrl) + + keyResolver.EXPECT().ResolveKey(testDID, nil, resolver.NutsSigningKeyType).Return(vdr.TestMethodDIDA.URI(), key.Public(), nil) + + w := New(keyResolver, keyStore, nil, jsonldManager, nil) + + resultingPresentation, err := w.BuildPresentation(ctx, []vc.VerifiableCredential{testCredential, testCredential}, PresentationOptions{Format: JSONLDPresentationFormat}, &testDID, false) + + assert.NoError(t, err) + assert.NotNil(t, resultingPresentation) + }) }) - t.Run("ok - multiple VCs", func(t *testing.T) { - ctrl := gomock.NewController(t) + t.Run("JWT", func(t *testing.T) { + options := PresentationOptions{Format: JWTPresentationFormat} + t.Run("ok - one VC", func(t *testing.T) { + ctrl := gomock.NewController(t) - keyResolver := resolver.NewMockKeyResolver(ctrl) + keyResolver := resolver.NewMockKeyResolver(ctrl) + keyResolver.EXPECT().ResolveKey(testDID, nil, resolver.NutsSigningKeyType).Return(ssi.MustParseURI(kid), key.Public(), nil) - keyResolver.EXPECT().ResolveKey(testDID, nil, resolver.NutsSigningKeyType).Return(vdr.TestMethodDIDA.URI(), key.Public(), nil) + w := New(keyResolver, keyStore, nil, jsonldManager, nil) - w := New(keyResolver, keyStore, nil, jsonldManager, nil) + result, err := w.BuildPresentation(ctx, []vc.VerifiableCredential{testCredential}, options, &testDID, false) - resultingPresentation, err := w.BuildPresentation(ctx, []vc.VerifiableCredential{testCredential, testCredential}, options, &testDID, false) + require.NoError(t, err) + require.NotNil(t, result) + assert.Equal(t, JWTPresentationFormat, result.Format()) + assert.NotNil(t, result.JWT()) + }) + t.Run("ok - multiple VCs", func(t *testing.T) { + ctrl := gomock.NewController(t) - assert.NoError(t, err) - assert.NotNil(t, resultingPresentation) + keyResolver := resolver.NewMockKeyResolver(ctrl) + + keyResolver.EXPECT().ResolveKey(testDID, nil, resolver.NutsSigningKeyType).Return(vdr.TestMethodDIDA.URI(), key.Public(), nil) + + w := New(keyResolver, keyStore, nil, jsonldManager, nil) + + result, err := w.BuildPresentation(ctx, []vc.VerifiableCredential{testCredential, testCredential}, options, &testDID, false) + + require.NoError(t, err) + require.NotNil(t, result) + assert.Equal(t, JWTPresentationFormat, result.Format()) + assert.NotNil(t, result.JWT()) + }) + t.Run("optional proof options", func(t *testing.T) { + exp := time.Now().Local().Truncate(time.Second) + options := PresentationOptions{ + Format: JWTPresentationFormat, + ProofOptions: proof.ProofOptions{ + Expires: &exp, + Created: exp.Add(-1 * time.Hour), + }, + } + + ctrl := gomock.NewController(t) + + keyResolver := resolver.NewMockKeyResolver(ctrl) + keyResolver.EXPECT().ResolveKey(testDID, nil, resolver.NutsSigningKeyType).Return(ssi.MustParseURI(kid), key.Public(), nil) + + w := New(keyResolver, keyStore, nil, jsonldManager, nil) + + result, err := w.BuildPresentation(ctx, []vc.VerifiableCredential{testCredential}, options, &testDID, false) + + require.NoError(t, err) + 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()) + }) }) t.Run("validation", func(t *testing.T) { created := time.Now() @@ -149,6 +226,22 @@ func TestWallet_BuildPresentation(t *testing.T) { assert.EqualError(t, err, "invalid credential (id="+testCredential.ID.String()+"): failed") assert.Nil(t, resultingPresentation) }) + t.Run("unsupported format", func(t *testing.T) { + ctrl := gomock.NewController(t) + + keyResolver := resolver.NewMockKeyResolver(ctrl) + mockVerifier := verifier.NewMockVerifier(ctrl) + mockVerifier.EXPECT().Validate(gomock.Any(), gomock.Any()) + + keyResolver.EXPECT().ResolveKey(testDID, nil, resolver.NutsSigningKeyType).Return(ssi.MustParseURI(kid), key.Public(), nil) + + w := New(keyResolver, keyStore, mockVerifier, jsonldManager, nil) + + result, err := w.BuildPresentation(ctx, []vc.VerifiableCredential{testCredential}, PresentationOptions{Format: "paper"}, &testDID, true) + + assert.EqualError(t, err, "unsupported presentation proof format") + assert.Nil(t, result) + }) }) t.Run("deriving signer from VCs", func(t *testing.T) { options := PresentationOptions{ProofOptions: proof.ProofOptions{}} diff --git a/vcr/issuer/interface.go b/vcr/issuer/interface.go index f6e55c002a..f8c3a54324 100644 --- a/vcr/issuer/interface.go +++ b/vcr/issuer/interface.go @@ -47,9 +47,7 @@ type keyResolver interface { // Issuer is a role in the network for a party who issues credentials about a subject to a holder. type Issuer interface { // Issue issues a credential by signing an unsigned credential. - // The publish param indicates if the credendential should be published to the network. - // The public param instructs the Publisher to publish the param with a certain visibility. - Issue(ctx context.Context, unsignedCredential vc.VerifiableCredential, publish, public bool) (*vc.VerifiableCredential, error) + Issue(ctx context.Context, template vc.VerifiableCredential, options CredentialOptions) (*vc.VerifiableCredential, error) // Revoke revokes a credential by the provided type. // It requires access to the private key of the issuer which will be used to sign the revocation. // It returns an error when the credential is not issued by this node or is already revoked. @@ -86,3 +84,21 @@ type CredentialSearcher interface { // If the passed context is empty, it'll not be part of the search query on the DB. SearchCredential(credentialType ssi.URI, issuer did.DID, subject *ssi.URI) ([]vc.VerifiableCredential, error) } + +const ( + JSONLDCredentialFormat = vc.JSONLDCredentialProofFormat + JWTCredentialFormat = vc.JWTCredentialProofFormat + JSONLDPresentationFormat = vc.JSONLDPresentationProofFormat + JWTPresentationFormat = vc.JWTPresentationProofFormat +) + +// CredentialOptions specifies options for issuing a credential. +type CredentialOptions struct { + // Format specifies the proof format for the issued credential. If not set, it defaults to JSON-LD. + // Valid options are: ldp_vc or jwt_vc + Format string + // Publish param indicates if the credential should be published to the network. + Publish bool + // Public param instructs the Publisher to publish the param with a certain visibility. + Public bool +} diff --git a/vcr/issuer/issuer.go b/vcr/issuer/issuer.go index 100c6062e0..620d5a02c6 100644 --- a/vcr/issuer/issuer.go +++ b/vcr/issuer/issuer.go @@ -21,6 +21,7 @@ package issuer import ( "context" "encoding/json" + "errors" "fmt" "github.com/nuts-foundation/nuts-node/vcr/openid4vci" "github.com/nuts-foundation/nuts-node/vdr/resolver" @@ -89,8 +90,13 @@ type issuer struct { // Issue creates a new credential, signs, stores it. // If publish is true, it publishes the credential to the network using the configured Publisher // Use the public flag to pass the visibility settings to the Publisher. -func (i issuer) Issue(ctx context.Context, credentialOptions vc.VerifiableCredential, publish, public bool) (*vc.VerifiableCredential, error) { - createdVC, err := i.buildVC(ctx, credentialOptions) +func (i issuer) Issue(ctx context.Context, template vc.VerifiableCredential, options CredentialOptions) (*vc.VerifiableCredential, error) { + // Until further notice we don't support publishing JWT VCs, since they're not officially supported by Nuts yet. + if options.Publish && options.Format == JWTCredentialFormat { + return nil, errors.New("publishing VC JWTs is not supported") + } + + createdVC, err := i.buildVC(ctx, template, options) if err != nil { return nil, err } @@ -123,10 +129,10 @@ func (i issuer) Issue(ctx context.Context, credentialOptions vc.VerifiableCreden return nil, fmt.Errorf("unable to store the issued credential: %w", err) } - if publish { + if options.Publish { // Try to issue over OpenID4VCI if it's enabled and if the credential is not public // (public credentials are always published on the network). - if i.openidHandlerFn != nil && !public { + if i.openidHandlerFn != nil && !options.Public { success, err := i.issueUsingOpenID4VCI(ctx, *createdVC) if err != nil { // An error occurred, but it's not because the wallet/issuer doesn't support OpenID4VCI. @@ -145,7 +151,7 @@ func (i issuer) Issue(ctx context.Context, credentialOptions vc.VerifiableCreden Info("Wallet or issuer does not support OpenID4VCI, fallback to publish over Nuts network") } } - if err := i.networkPublisher.PublishCredential(ctx, *createdVC, public); err != nil { + if err := i.networkPublisher.PublishCredential(ctx, *createdVC, options.Public); err != nil { return nil, fmt.Errorf("unable to publish the issued credential: %w", err) } } @@ -179,12 +185,12 @@ func (i issuer) issueUsingOpenID4VCI(ctx context.Context, credential vc.Verifiab return true, i.vcrStore.StoreCredential(credential, nil) } -func (i issuer) buildVC(ctx context.Context, credentialOptions vc.VerifiableCredential) (*vc.VerifiableCredential, error) { - if len(credentialOptions.Type) != 1 { +func (i issuer) buildVC(ctx context.Context, template vc.VerifiableCredential, options CredentialOptions) (*vc.VerifiableCredential, error) { + if len(template.Type) != 1 { return nil, core.InvalidInputError("can only issue credential with 1 type") } - issuerDID, err := did.ParseDID(credentialOptions.Issuer.String()) + issuerDID, err := did.ParseDID(template.Issuer.String()) if err != nil { return nil, fmt.Errorf("failed to parse issuer: %w", err) } @@ -201,13 +207,16 @@ func (i issuer) buildVC(ctx context.Context, credentialOptions vc.VerifiableCred credentialID := ssi.MustParseURI(fmt.Sprintf("%s#%s", issuerDID.String(), uuid.New().String())) unsignedCredential := vc.VerifiableCredential{ - Context: credentialOptions.Context, + Context: template.Context, ID: &credentialID, - Type: credentialOptions.Type, - CredentialSubject: credentialOptions.CredentialSubject, - Issuer: credentialOptions.Issuer, - ExpirationDate: credentialOptions.ExpirationDate, - IssuanceDate: TimeFunc(), + Type: template.Type, + CredentialSubject: template.CredentialSubject, + Issuer: template.Issuer, + ExpirationDate: template.ExpirationDate, + IssuanceDate: template.IssuanceDate, + } + if unsignedCredential.IssuanceDate.IsZero() { + unsignedCredential.IssuanceDate = TimeFunc() } if !unsignedCredential.ContainsContext(vc.VCContextV1URI()) { unsignedCredential.Context = append(unsignedCredential.Context, vc.VCContextV1URI()) @@ -218,28 +227,34 @@ func (i issuer) buildVC(ctx context.Context, credentialOptions vc.VerifiableCred unsignedCredential.Type = append(unsignedCredential.Type, defaultType) } + switch options.Format { + case JWTCredentialFormat: + return vc.CreateJWTVerifiableCredential(ctx, unsignedCredential, func(ctx context.Context, claims map[string]interface{}, headers map[string]interface{}) (string, error) { + return i.keyStore.SignJWT(ctx, claims, headers, key) + }) + case "": + fallthrough + case JSONLDCredentialFormat: + return i.buildJSONLDCredential(ctx, unsignedCredential, key) + default: + return nil, errors.New("unsupported credential proof format") + } +} + +func (i issuer) buildJSONLDCredential(ctx context.Context, unsignedCredential vc.VerifiableCredential, key crypto.Key) (*vc.VerifiableCredential, error) { credentialAsMap := map[string]interface{}{} b, _ := json.Marshal(unsignedCredential) _ = json.Unmarshal(b, &credentialAsMap) - // Set created date to the issuanceDate if set - created := TimeFunc() - if !credentialOptions.IssuanceDate.IsZero() { - created = credentialOptions.IssuanceDate - } - proofOptions := proof.ProofOptions{Created: created} + proofOptions := proof.ProofOptions{Created: unsignedCredential.IssuanceDate} webSig := signature.JSONWebSignature2020{ContextLoader: i.jsonldManager.DocumentLoader(), Signer: i.keyStore} signingResult, err := proof.NewLDProof(proofOptions).Sign(ctx, credentialAsMap, webSig, key) if err != nil { return nil, err } - - b, _ = json.Marshal(signingResult) - signedCredential := &vc.VerifiableCredential{} - _ = json.Unmarshal(b, signedCredential) - - return signedCredential, nil + credentialJSON, _ := json.Marshal(signingResult) + return vc.ParseVerifiableCredential(string(credentialJSON)) } func (i issuer) Revoke(ctx context.Context, credentialID ssi.URI) (*credential.Revocation, error) { diff --git a/vcr/issuer/issuer_test.go b/vcr/issuer/issuer_test.go index ab5f4b3569..d978de26c7 100644 --- a/vcr/issuer/issuer_test.go +++ b/vcr/issuer/issuer_test.go @@ -20,6 +20,7 @@ package issuer import ( "context" + crypt "crypto" "encoding/json" "errors" "fmt" @@ -54,57 +55,110 @@ func Test_issuer_buildVC(t *testing.T) { issuerDID, _ := did.ParseDID(issuerID.String()) ctx := audit.TestContext() - t.Run("it builds and signs a VC", func(t *testing.T) { - ctrl := gomock.NewController(t) - kid := "did:nuts:123#abc" + const kid = "did:nuts:123#abc" + const subjectDID = "did:nuts:456" + schemaOrgContext := ssi.MustParseURI("https://schema.org") + issuance, err := time.Parse(time.RFC3339, "2022-01-02T12:00:00Z") + require.NoError(t, err) + + expirationDate := issuance.Add(time.Hour) + template := vc.VerifiableCredential{ + Context: []ssi.URI{schemaOrgContext}, + Type: []ssi.URI{credentialType}, + Issuer: issuerID, + IssuanceDate: issuance, + ExpirationDate: &expirationDate, + CredentialSubject: []interface{}{map[string]interface{}{ + "id": subjectDID, + }}, + } + keyStore := crypto.NewMemoryCryptoInstance() + signingKey, err := keyStore.New(ctx, func(key crypt.PublicKey) (string, error) { + return kid, nil + }) + require.NoError(t, err) - keyResolverMock := NewMockkeyResolver(ctrl) - keyResolverMock.EXPECT().ResolveAssertionKey(ctx, gomock.Any()).Return(crypto.NewTestKey(kid), nil) - jsonldManager := jsonld.NewTestJSONLDManager(t) - sut := issuer{keyResolver: keyResolverMock, jsonldManager: jsonldManager, keyStore: crypto.NewMemoryCryptoInstance()} - schemaOrgContext := ssi.MustParseURI("https://schema.org") + t.Run("JSON-LD", func(t *testing.T) { + t.Run("ok", func(t *testing.T) { + ctrl := gomock.NewController(t) + keyResolverMock := NewMockkeyResolver(ctrl) + keyResolverMock.EXPECT().ResolveAssertionKey(ctx, gomock.Any()).Return(signingKey, nil) + jsonldManager := jsonld.NewTestJSONLDManager(t) + sut := issuer{keyResolver: keyResolverMock, jsonldManager: jsonldManager, keyStore: keyStore} - issuance, err := time.Parse(time.RFC3339, "2022-01-02T12:00:00Z") - assert.NoError(t, err) + result, err := sut.buildVC(ctx, template, CredentialOptions{Format: JSONLDCredentialFormat}) + require.NoError(t, err) + require.NotNil(t, result) + assert.Contains(t, result.Type, credentialType, "expected vc to be of right type") + assert.Equal(t, JSONLDCredentialFormat, result.Format()) + assert.Equal(t, issuerID.String(), result.Issuer.String(), "expected correct issuer") + assert.Contains(t, result.Context, schemaOrgContext) + assert.Contains(t, result.Context, vc.VCContextV1URI()) + // Assert proof + proofs, _ := result.Proofs() + assert.Equal(t, kid, proofs[0].VerificationMethod.String(), "expected to be signed with the kid") + assert.Equal(t, issuance, proofs[0].Created) + }) + t.Run("is default", func(t *testing.T) { + ctrl := gomock.NewController(t) - credentialOptions := vc.VerifiableCredential{ - Context: []ssi.URI{schemaOrgContext}, - Type: []ssi.URI{credentialType}, - Issuer: issuerID, - IssuanceDate: issuance, - CredentialSubject: []interface{}{map[string]interface{}{ - "id": "did:nuts:456", - }}, - } - result, err := sut.buildVC(ctx, credentialOptions) - require.NoError(t, err) - require.NotNil(t, result) - assert.Contains(t, result.Type, credentialType, "expected vc to be of right type") - proofs, _ := result.Proofs() - assert.Equal(t, kid, proofs[0].VerificationMethod.String(), "expected to be signed with the kid") - assert.Equal(t, issuerID.String(), result.Issuer.String(), "expected correct issuer") - assert.Contains(t, result.Context, schemaOrgContext) - assert.Contains(t, result.Context, vc.VCContextV1URI()) - assert.Equal(t, issuance, proofs[0].Created) + keyResolverMock := NewMockkeyResolver(ctrl) + keyResolverMock.EXPECT().ResolveAssertionKey(ctx, gomock.Any()).Return(signingKey, nil) + jsonldManager := jsonld.NewTestJSONLDManager(t) + sut := issuer{keyResolver: keyResolverMock, jsonldManager: jsonldManager, keyStore: keyStore} + + result, err := sut.buildVC(ctx, template, CredentialOptions{}) + require.NoError(t, err) + require.NotNil(t, result) + assert.Equal(t, JSONLDCredentialFormat, result.Format()) + }) + }) + t.Run("JWT", func(t *testing.T) { + t.Run("ok", func(t *testing.T) { + ctrl := gomock.NewController(t) + keyResolverMock := NewMockkeyResolver(ctrl) + keyResolverMock.EXPECT().ResolveAssertionKey(ctx, gomock.Any()).Return(signingKey, nil) + jsonldManager := jsonld.NewTestJSONLDManager(t) + sut := issuer{keyResolver: keyResolverMock, jsonldManager: jsonldManager, keyStore: keyStore} + + result, err := sut.buildVC(ctx, template, CredentialOptions{Format: JWTCredentialFormat}) + + require.NoError(t, err) + require.NotNil(t, result) + assert.Equal(t, JWTCredentialFormat, result.Format()) + assert.Contains(t, result.Type, credentialType, "expected vc to be of right type") + assert.Contains(t, result.Context, schemaOrgContext) + assert.Contains(t, result.Context, vc.VCContextV1URI()) + assert.Equal(t, template.IssuanceDate.Local(), result.IssuanceDate.Local()) + assert.Equal(t, template.ExpirationDate.Local(), result.ExpirationDate.Local()) + assert.Equal(t, template.Issuer, result.Issuer) + assert.Equal(t, template.CredentialSubject, result.CredentialSubject) + 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()) + }) }) t.Run("it does not add the default context twice", func(t *testing.T) { ctrl := gomock.NewController(t) - kid := "did:nuts:123#abc" keyResolverMock := NewMockkeyResolver(ctrl) - keyResolverMock.EXPECT().ResolveAssertionKey(ctx, gomock.Any()).Return(crypto.NewTestKey(kid), nil) + keyResolverMock.EXPECT().ResolveAssertionKey(ctx, gomock.Any()).Return(signingKey, nil) jsonldManager := jsonld.NewTestJSONLDManager(t) - sut := issuer{keyResolver: keyResolverMock, jsonldManager: jsonldManager, keyStore: crypto.NewMemoryCryptoInstance()} + sut := issuer{keyResolver: keyResolverMock, jsonldManager: jsonldManager, keyStore: keyStore} - credentialOptions := vc.VerifiableCredential{ + template := vc.VerifiableCredential{ Context: []ssi.URI{vc.VCContextV1URI()}, Type: []ssi.URI{credentialType}, Issuer: issuerID, IssuanceDate: time.Now(), } - result, err := sut.buildVC(ctx, credentialOptions) + result, err := sut.buildVC(ctx, template, CredentialOptions{}) require.NoError(t, err) require.NotNil(t, result) @@ -116,10 +170,10 @@ func Test_issuer_buildVC(t *testing.T) { t.Run("wrong amount of credential types", func(t *testing.T) { sut := issuer{} - credentialOptions := vc.VerifiableCredential{ + template := vc.VerifiableCredential{ Type: []ssi.URI{}, } - result, err := sut.buildVC(ctx, credentialOptions) + result, err := sut.buildVC(ctx, template, CredentialOptions{}) assert.ErrorIs(t, err, core.InvalidInputError("can only issue credential with 1 type")) assert.Nil(t, result) @@ -128,14 +182,27 @@ func Test_issuer_buildVC(t *testing.T) { t.Run("missing issuer", func(t *testing.T) { sut := issuer{} - credentialOptions := vc.VerifiableCredential{ + template := vc.VerifiableCredential{ Type: []ssi.URI{credentialType}, } - result, err := sut.buildVC(ctx, credentialOptions) + result, err := sut.buildVC(ctx, template, CredentialOptions{}) assert.ErrorIs(t, err, did.ErrInvalidDID) assert.Nil(t, result) }) + t.Run("unsupported proof format", func(t *testing.T) { + ctrl := gomock.NewController(t) + + keyResolverMock := NewMockkeyResolver(ctrl) + keyResolverMock.EXPECT().ResolveAssertionKey(ctx, gomock.Any()).Return(signingKey, nil) + jsonldManager := jsonld.NewTestJSONLDManager(t) + sut := issuer{keyResolver: keyResolverMock, jsonldManager: jsonldManager, keyStore: keyStore} + + result, err := sut.buildVC(ctx, template, CredentialOptions{Format: "paper"}) + + assert.EqualError(t, err, "unsupported credential proof format") + assert.Nil(t, result) + }) }) t.Run("error - returned from used services", func(t *testing.T) { @@ -146,11 +213,11 @@ func Test_issuer_buildVC(t *testing.T) { keyResolverMock.EXPECT().ResolveAssertionKey(ctx, *issuerDID).Return(nil, errors.New("b00m!")) sut := issuer{keyResolver: keyResolverMock} - credentialOptions := vc.VerifiableCredential{ + template := vc.VerifiableCredential{ Type: []ssi.URI{credentialType}, Issuer: issuerID, } - _, err := sut.buildVC(ctx, credentialOptions) + _, err := sut.buildVC(ctx, template, CredentialOptions{}) assert.EqualError(t, err, "failed to sign credential: could not resolve an assertionKey for issuer: b00m!") }) @@ -161,11 +228,11 @@ func Test_issuer_buildVC(t *testing.T) { keyResolverMock.EXPECT().ResolveAssertionKey(ctx, *issuerDID).Return(nil, resolver.ErrNotFound) sut := issuer{keyResolver: keyResolverMock} - credentialOptions := vc.VerifiableCredential{ + template := vc.VerifiableCredential{ Type: []ssi.URI{credentialType}, Issuer: issuerID, } - _, err := sut.buildVC(ctx, credentialOptions) + _, err := sut.buildVC(ctx, template, CredentialOptions{}) assert.ErrorIs(t, err, core.InvalidInputError("failed to sign credential: could not resolve an assertionKey for issuer: unable to find the DID document")) }) }) @@ -177,7 +244,7 @@ func Test_issuer_Issue(t *testing.T) { issuerKeyID := issuerDID.String() + "#abc" holderDID := did.MustParseDID("did:nuts:456") - credentialOptions := vc.VerifiableCredential{ + template := vc.VerifiableCredential{ Context: []ssi.URI{credential.NutsV1ContextURI}, Type: []ssi.URI{credentialType}, Issuer: issuerDID.URI(), @@ -202,7 +269,10 @@ func Test_issuer_Issue(t *testing.T) { keyStore: crypto.NewMemoryCryptoInstance(), } - result, err := sut.Issue(ctx, credentialOptions, false, true) + result, err := sut.Issue(ctx, template, CredentialOptions{ + Publish: false, + Public: true, + }) require.NoError(t, err) assert.Contains(t, result.Type, credentialType, "expected vc to be of right type") proofs, _ := result.Proofs() @@ -214,6 +284,18 @@ func Test_issuer_Issue(t *testing.T) { assert.True(t, trustConfig.IsTrusted(credentialType, result.Issuer)) }) + t.Run("publishing JWT VCs is disallowed", func(t *testing.T) { + sut := issuer{} + + result, err := sut.Issue(ctx, template, CredentialOptions{ + Publish: true, + Public: true, + Format: JWTCredentialFormat, + }) + require.EqualError(t, err, "publishing VC JWTs is not supported") + assert.Nil(t, result) + }) + t.Run("OpenID4VCI", func(t *testing.T) { const walletIdentifier = "http://example.com/wallet" t.Run("ok - publish over OpenID4VCI fails - fallback to network", func(t *testing.T) { @@ -244,7 +326,10 @@ func Test_issuer_Issue(t *testing.T) { networkPublisher: publisher, } - result, err := sut.Issue(ctx, credentialOptions, true, false) + result, err := sut.Issue(ctx, template, CredentialOptions{ + Publish: true, + Public: false, + }) require.NoError(t, err) assert.NotNil(t, result) @@ -266,7 +351,10 @@ func Test_issuer_Issue(t *testing.T) { networkPublisher: publisher, } - result, err := sut.Issue(ctx, credentialOptions, true, false) + result, err := sut.Issue(ctx, template, CredentialOptions{ + Publish: true, + Public: false, + }) require.NoError(t, err) assert.NotNil(t, result) @@ -291,7 +379,10 @@ func Test_issuer_Issue(t *testing.T) { networkPublisher: publisher, } - result, err := sut.Issue(ctx, credentialOptions, true, false) + result, err := sut.Issue(ctx, template, CredentialOptions{ + Publish: true, + Public: false, + }) require.NoError(t, err) assert.NotNil(t, result) @@ -324,7 +415,10 @@ func Test_issuer_Issue(t *testing.T) { vcrStore: vcrStore, } - result, err := sut.Issue(ctx, credentialOptions, true, false) + result, err := sut.Issue(ctx, template, CredentialOptions{ + Publish: true, + Public: false, + }) require.NoError(t, err) assert.NotNil(t, result) @@ -346,7 +440,10 @@ func Test_issuer_Issue(t *testing.T) { keyStore: crypto.NewMemoryCryptoInstance(), } - result, err := sut.Issue(ctx, credentialOptions, false, true) + result, err := sut.Issue(ctx, template, CredentialOptions{ + Publish: false, + Public: true, + }) assert.EqualError(t, err, "unable to store the issued credential: b00m!") assert.Nil(t, result) }) @@ -366,7 +463,10 @@ func Test_issuer_Issue(t *testing.T) { keyStore: crypto.NewMemoryCryptoInstance(), } - result, err := sut.Issue(ctx, credentialOptions, true, true) + result, err := sut.Issue(ctx, template, CredentialOptions{ + Publish: true, + Public: true, + }) assert.EqualError(t, err, "unable to publish the issued credential: b00m!") assert.Nil(t, result) }) @@ -379,7 +479,10 @@ func Test_issuer_Issue(t *testing.T) { Issuer: issuerDID.URI(), } - result, err := sut.Issue(ctx, credentialOptions, true, true) + result, err := sut.Issue(ctx, credentialOptions, CredentialOptions{ + Publish: true, + Public: true, + }) assert.EqualError(t, err, "can only issue credential with 1 type") assert.Nil(t, result) @@ -393,12 +496,15 @@ func Test_issuer_Issue(t *testing.T) { mockStore := NewMockStore(ctrl) sut := issuer{keyResolver: keyResolverMock, store: mockStore, jsonldManager: jsonldManager, keyStore: crypto.NewMemoryCryptoInstance()} - invalidCred := credentialOptions + invalidCred := template invalidCred.CredentialSubject = []interface{}{ map[string]interface{}{"foo": "bar"}, } - result, err := sut.Issue(ctx, invalidCred, true, true) + result, err := sut.Issue(ctx, invalidCred, CredentialOptions{ + Publish: true, + Public: true, + }) assert.EqualError(t, err, "validation failed: invalid property: Dropping property that did not expand into an absolute IRI or keyword.") assert.Nil(t, result) }) diff --git a/vcr/issuer/leia_store_test.go b/vcr/issuer/leia_store_test.go index af7d8162f2..d12a8b6ace 100644 --- a/vcr/issuer/leia_store_test.go +++ b/vcr/issuer/leia_store_test.go @@ -88,51 +88,52 @@ func TestLeiaIssuerStore_StoreCredential(t *testing.T) { } func Test_leiaStore_StoreAndSearchCredential(t *testing.T) { - vcToStore := vc.VerifiableCredential{} - _ = json.Unmarshal([]byte(jsonld.TestCredential), &vcToStore) + expected := vc.VerifiableCredential{} + _ = json.Unmarshal([]byte(jsonld.TestCredential), &expected) + expectedJSON, _ := expected.MarshalJSON() t.Run("store", func(t *testing.T) { sut := newStore(t) - err := sut.StoreCredential(vcToStore) + err := sut.StoreCredential(expected) assert.NoError(t, err) t.Run("and search", func(t *testing.T) { - issuerDID, _ := did.ParseDID(vcToStore.Issuer.String()) + issuerDID, _ := did.ParseDID(expected.Issuer.String()) subjectID := ssi.MustParseURI("did:nuts:GvkzxsezHvEc8nGhgz6Xo3jbqkHwswLmWw3CYtCm7hAW") t.Run("for all issued credentials for a issuer", func(t *testing.T) { - res, err := sut.SearchCredential(vcToStore.Type[0], *issuerDID, nil) + res, err := sut.SearchCredential(expected.Type[0], *issuerDID, nil) assert.NoError(t, err) require.Len(t, res, 1) foundVC := res[0] - assert.Equal(t, vcToStore, foundVC) + require.NoError(t, err) + foundJSON, _ := foundVC.MarshalJSON() + assert.JSONEq(t, string(expectedJSON), string(foundJSON)) }) t.Run("for all issued credentials for a issuer and subject", func(t *testing.T) { - res, err := sut.SearchCredential(vcToStore.Type[0], *issuerDID, &subjectID) + res, err := sut.SearchCredential(expected.Type[0], *issuerDID, &subjectID) assert.NoError(t, err) require.Len(t, res, 1) - - foundVC := res[0] - assert.Equal(t, vcToStore, foundVC) + foundJSON, _ := res[0].MarshalJSON() + assert.JSONEq(t, string(expectedJSON), string(foundJSON)) }) t.Run("without context", func(t *testing.T) { - res, err := sut.SearchCredential(vcToStore.Type[0], *issuerDID, nil) + res, err := sut.SearchCredential(expected.Type[0], *issuerDID, nil) assert.NoError(t, err) require.Len(t, res, 1) - - foundVC := res[0] - assert.Equal(t, vcToStore, foundVC) + foundJSON, _ := res[0].MarshalJSON() + assert.JSONEq(t, string(expectedJSON), string(foundJSON)) }) t.Run("no results", func(t *testing.T) { t.Run("unknown issuer", func(t *testing.T) { unknownIssuerDID, _ := did.ParseDID("did:nuts:123") - res, err := sut.SearchCredential(vcToStore.Type[0], *unknownIssuerDID, nil) + res, err := sut.SearchCredential(expected.Type[0], *unknownIssuerDID, nil) assert.NoError(t, err) require.Len(t, res, 0) }) @@ -146,7 +147,7 @@ func Test_leiaStore_StoreAndSearchCredential(t *testing.T) { t.Run("unknown subject", func(t *testing.T) { unknownSubject := ssi.MustParseURI("did:nuts:unknown") - res, err := sut.SearchCredential(vcToStore.Type[0], *issuerDID, &unknownSubject) + res, err := sut.SearchCredential(expected.Type[0], *issuerDID, &unknownSubject) assert.NoError(t, err) require.Len(t, res, 0) }) @@ -158,22 +159,24 @@ func Test_leiaStore_StoreAndSearchCredential(t *testing.T) { } func Test_leiaStore_GetCredential(t *testing.T) { - vcToGet := vc.VerifiableCredential{} - _ = json.Unmarshal([]byte(jsonld.TestCredential), &vcToGet) + expected := vc.VerifiableCredential{} + _ = json.Unmarshal([]byte(jsonld.TestCredential), &expected) + expectedJSON, _ := expected.MarshalJSON() t.Run("with a known credential", func(t *testing.T) { store := newStore(t) - assert.NoError(t, store.StoreCredential(vcToGet)) + assert.NoError(t, store.StoreCredential(expected)) t.Run("it finds the credential by id", func(t *testing.T) { - foundCredential, err := store.GetCredential(*vcToGet.ID) - assert.NoError(t, err) - assert.Equal(t, *foundCredential, vcToGet) + foundCredential, err := store.GetCredential(*expected.ID) + require.NoError(t, err) + foundJSON, _ := foundCredential.MarshalJSON() + assert.JSONEq(t, string(expectedJSON), string(foundJSON)) }) }) t.Run("no results", func(t *testing.T) { store := newStore(t) - foundCredential, err := store.GetCredential(*vcToGet.ID) + foundCredential, err := store.GetCredential(*expected.ID) assert.ErrorIs(t, err, types.ErrNotFound) assert.Nil(t, foundCredential) }) @@ -181,17 +184,17 @@ func Test_leiaStore_GetCredential(t *testing.T) { t.Run("multiple results", func(t *testing.T) { store := newStore(t) // store once - assert.NoError(t, store.StoreCredential(vcToGet)) + assert.NoError(t, store.StoreCredential(expected)) // store twice lstore := store.(*leiaIssuerStore) rawStructWithSameID := struct { ID *ssi.URI `json:"id,omitempty"` - }{ID: vcToGet.ID} + }{ID: expected.ID} asBytes, _ := json.Marshal(rawStructWithSameID) lstore.issuedCollection().Add([]leia.Document{asBytes}) t.Run("it fails", func(t *testing.T) { - foundCredential, err := store.GetCredential(*vcToGet.ID) + foundCredential, err := store.GetCredential(*expected.ID) assert.ErrorIs(t, err, types.ErrMultipleFound) assert.Nil(t, foundCredential) }) diff --git a/vcr/issuer/mock.go b/vcr/issuer/mock.go index d41c9326d3..5724b935df 100644 --- a/vcr/issuer/mock.go +++ b/vcr/issuer/mock.go @@ -134,18 +134,18 @@ func (m *MockIssuer) EXPECT() *MockIssuerMockRecorder { } // Issue mocks base method. -func (m *MockIssuer) Issue(ctx context.Context, unsignedCredential vc.VerifiableCredential, publish, public bool) (*vc.VerifiableCredential, error) { +func (m *MockIssuer) Issue(ctx context.Context, template vc.VerifiableCredential, options CredentialOptions) (*vc.VerifiableCredential, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Issue", ctx, unsignedCredential, publish, public) + ret := m.ctrl.Call(m, "Issue", ctx, template, options) ret0, _ := ret[0].(*vc.VerifiableCredential) ret1, _ := ret[1].(error) return ret0, ret1 } // Issue indicates an expected call of Issue. -func (mr *MockIssuerMockRecorder) Issue(ctx, unsignedCredential, publish, public any) *gomock.Call { +func (mr *MockIssuerMockRecorder) Issue(ctx, template, options any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Issue", reflect.TypeOf((*MockIssuer)(nil).Issue), ctx, unsignedCredential, publish, public) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Issue", reflect.TypeOf((*MockIssuer)(nil).Issue), ctx, template, options) } // Revoke mocks base method. diff --git a/vcr/store.go b/vcr/store.go index 4c7ae0b292..e74c467711 100644 --- a/vcr/store.go +++ b/vcr/store.go @@ -44,7 +44,7 @@ func (c *vcr) StoreCredential(credential vc.VerifiableCredential, validAt *time. if credential.ID != nil { existingCredential, err := c.find(*credential.ID) if err == nil { - if reflect.DeepEqual(existingCredential, credential) { + if credentialsEqual(existingCredential, credential) { log.Logger(). WithField(core.LogFieldCredentialID, *credential.ID). Info("Credential already exists") @@ -64,6 +64,18 @@ func (c *vcr) StoreCredential(credential vc.VerifiableCredential, validAt *time. return c.writeCredential(credential) } +func credentialsEqual(a vc.VerifiableCredential, b vc.VerifiableCredential) bool { + // go-leia returns pretty-printed JSON documents, so the verifiable credentials have different `raw` properties. + // thus we need to unmarshal the verifiable credentials into maps and compare those. + aJSON, _ := json.Marshal(a) + bJSON, _ := json.Marshal(b) + aMap := map[string]interface{}{} + bMap := map[string]interface{}{} + _ = json.Unmarshal(aJSON, &aMap) + _ = json.Unmarshal(bJSON, &bMap) + return reflect.DeepEqual(aMap, bMap) +} + func (c *vcr) writeCredential(subject vc.VerifiableCredential) error { vcType := "VerifiableCredential" customTypes := credential.ExtractTypes(subject) diff --git a/vcr/test/formats_integration_test.go b/vcr/test/formats_integration_test.go new file mode 100644 index 0000000000..f1e726ad04 --- /dev/null +++ b/vcr/test/formats_integration_test.go @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2023 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 test + +import ( + "context" + "encoding/json" + "github.com/nuts-foundation/go-did/vc" + "github.com/nuts-foundation/nuts-node/audit" + "github.com/nuts-foundation/nuts-node/test/node" + "github.com/nuts-foundation/nuts-node/vcr" + v2 "github.com/nuts-foundation/nuts-node/vcr/api/vcr/v2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "net/http/httptest" + "strings" + "testing" +) + +// TestCredentialFormats tests issuing and verifying different VC. +func TestCredentialFormats(t *testing.T) { + ctx := audit.TestContext() + _, system := node.StartServer(t) + + issuerDID := registerDID(t, system) + subjectDID := registerDID(t, system) + publish := false + credentialRequestTemplate := v2.IssueVCRequest{ + Type: "NutsOrganizationCredential", + CredentialSubject: map[string]interface{}{ + "id": subjectDID.String(), + "organization": map[string]interface{}{ + "name": "Nuts Foundation", + "city": "Notendam", + }, + }, + Issuer: issuerDID.String(), + PublishToNetwork: &publish, + } + vcrAPI := v2.Wrapper{VCR: system.FindEngineByName("vcr").(vcr.VCR)} + + t.Run("VC in JSON-LD format", func(t *testing.T) { + // Issuance + credentialRequest := credentialRequestTemplate + credential := issueVC(t, vcrAPI, ctx, credentialRequest) + assert.True(t, strings.HasPrefix(credential.Raw(), "{"), "expected JSON-LD VC response") + // Verification + verifyVC(t, ctx, credential, vcrAPI) + }) + t.Run("VC in JWT format", func(t *testing.T) { + // Issuance + var format v2.IssueVCRequestFormat = "jwt_vc" + credentialRequest := credentialRequestTemplate + credentialRequest.Format = &format + credential := issueVC(t, vcrAPI, ctx, credentialRequest) + assert.True(t, strings.HasPrefix(credential.Raw(), `ey`), "expected JWT VC response") + // Verification + verifyVC(t, ctx, credential, vcrAPI) + }) + t.Run("VP in JSON-LD format, containing VC in JSON-LD format", func(t *testing.T) { + credential := issueVC(t, vcrAPI, ctx, credentialRequestTemplate) + // Issuance + presentation := createVP(t, ctx, credential, "", vcrAPI) // empty string = default format + assert.True(t, strings.HasPrefix(presentation.Raw(), "{"), "expected JSON-LD VP response") + assert.True(t, strings.HasPrefix(presentation.VerifiableCredential[0].Raw(), "{"), "expected JSON-LD VC in VP response") + // Verification + verifyVP(t, ctx, presentation, vcrAPI) + }) + t.Run("VP in JSON-LD format, containing VC in JWT format", func(t *testing.T) { + var format v2.IssueVCRequestFormat = "jwt_vc" + credentialRequest := credentialRequestTemplate + credentialRequest.Format = &format + credential := issueVC(t, vcrAPI, ctx, credentialRequest) + // Issuance + presentation := createVP(t, ctx, credential, "", vcrAPI) // empty string = default format + assert.True(t, strings.HasPrefix(presentation.Raw(), "{"), "expected JSON-LD VP response") + assert.True(t, strings.HasPrefix(presentation.VerifiableCredential[0].Raw(), "ey"), "expected JWT VC in VP response") + // Verification + verifyVP(t, ctx, presentation, vcrAPI) + }) + t.Run("VP in JWT format, containing VC in JWT format", func(t *testing.T) { + var format v2.IssueVCRequestFormat = "jwt_vc" + credentialRequest := credentialRequestTemplate + credentialRequest.Format = &format + credential := issueVC(t, vcrAPI, ctx, credentialRequest) + // Issuance + presentation := createVP(t, ctx, credential, vc.JWTPresentationProofFormat, vcrAPI) + assert.True(t, strings.HasPrefix(presentation.Raw(), "ey"), "expected JWT VP response") + assert.True(t, strings.HasPrefix(presentation.VerifiableCredential[0].Raw(), "ey"), "expected JWT VC in VP response") + // Verification + verifyVP(t, ctx, presentation, vcrAPI) + }) + t.Run("VP in JWT format, containing VC in JSON-LD format", func(t *testing.T) { + credential := issueVC(t, vcrAPI, ctx, credentialRequestTemplate) + // Issuance + presentation := createVP(t, ctx, credential, vc.JWTPresentationProofFormat, vcrAPI) + assert.True(t, strings.HasPrefix(presentation.Raw(), "ey"), "expected JWT VP response") + assert.True(t, strings.HasPrefix(presentation.VerifiableCredential[0].Raw(), "{"), "expected JWT VC in VP response") + // Verification + verifyVP(t, ctx, presentation, vcrAPI) + }) +} + +func createVP(t *testing.T, ctx context.Context, credential v2.VerifiableCredential, format string, vcrAPI v2.Wrapper) vc.VerifiablePresentation { + request := v2.CreateVPJSONRequestBody{ + VerifiableCredentials: []v2.VerifiableCredential{credential}, + } + if format != "" { + f := v2.CreateVPRequestFormat(format) + request.Format = &f + } + response, err := vcrAPI.CreateVP(ctx, v2.CreateVPRequestObject{Body: &request}) + require.NoError(t, err) + httpResponse := httptest.NewRecorder() + require.NoError(t, response.VisitCreateVPResponse(httpResponse)) + var result vc.VerifiablePresentation + err = json.Unmarshal(httpResponse.Body.Bytes(), &result) + require.NoError(t, err) + return result +} + +func issueVC(t *testing.T, vcrAPI v2.Wrapper, ctx context.Context, credentialRequest v2.IssueVCRequest) v2.VerifiableCredential { + response, err := vcrAPI.IssueVC(ctx, v2.IssueVCRequestObject{Body: &credentialRequest}) + require.NoError(t, err) + httpResponse := httptest.NewRecorder() + require.NoError(t, response.VisitIssueVCResponse(httpResponse)) + var credential v2.VerifiableCredential + err = json.Unmarshal(httpResponse.Body.Bytes(), &credential) + require.NoError(t, err) + return credential +} + +func verifyVC(t *testing.T, ctx context.Context, credential vc.VerifiableCredential, vcrAPI v2.Wrapper) { + verifyResponse, err := vcrAPI.VerifyVC(ctx, v2.VerifyVCRequestObject{Body: &v2.VerifyVCJSONRequestBody{ + VerifiableCredential: credential, + }}) + require.NoError(t, err) + assert.True(t, verifyResponse.(v2.VerifyVC200JSONResponse).Validity) + if !assert.Nil(t, verifyResponse.(v2.VerifyVC200JSONResponse).Message) { + t.Log(*(verifyResponse.(v2.VerifyVC200JSONResponse).Message)) + } +} + +func verifyVP(t *testing.T, ctx context.Context, presentation vc.VerifiablePresentation, vcrAPI v2.Wrapper) { + verifyResponse, err := vcrAPI.VerifyVP(ctx, v2.VerifyVPRequestObject{Body: &v2.VerifyVPJSONRequestBody{ + VerifiablePresentation: presentation, + }}) + require.NoError(t, err) + assert.True(t, verifyResponse.(v2.VerifyVP200JSONResponse).Validity) + assert.Nil(t, verifyResponse.(v2.VerifyVP200JSONResponse).Message) +} diff --git a/vcr/test/openid4vci_integration_test.go b/vcr/test/openid4vci_integration_test.go index c1e6d76116..fd9a0f943c 100644 --- a/vcr/test/openid4vci_integration_test.go +++ b/vcr/test/openid4vci_integration_test.go @@ -24,6 +24,7 @@ import ( "github.com/nuts-foundation/nuts-node/core" httpModule "github.com/nuts-foundation/nuts-node/http" "github.com/nuts-foundation/nuts-node/network/log" + "github.com/nuts-foundation/nuts-node/vcr/issuer" "github.com/nuts-foundation/nuts-node/vcr/openid4vci" "github.com/nuts-foundation/nuts-node/vdr" "github.com/nuts-foundation/nuts-node/vdr/didnuts" @@ -69,7 +70,10 @@ func TestOpenID4VCIHappyFlow(t *testing.T) { "id": holderDID.URI().String(), "purposeOfUse": "test", }) - issuedVC, err := vcrService.Issuer().Issue(ctx, credential, true, false) + issuedVC, err := vcrService.Issuer().Issue(ctx, credential, issuer.CredentialOptions{ + Publish: true, + Public: false, + }) require.NoError(t, err) require.NotNil(t, issuedVC) @@ -124,7 +128,10 @@ func TestOpenID4VCIConnectionReuse(t *testing.T) { wg.Add(1) go func() { defer wg.Done() - _, err := vcrService.Issuer().Issue(ctx, credential, true, false) + _, err := vcrService.Issuer().Issue(ctx, credential, issuer.CredentialOptions{ + Publish: true, + Public: false, + }) if err != nil { errChan <- err return @@ -200,7 +207,10 @@ func TestOpenID4VCIDisabled(t *testing.T) { }) vcrService := system.FindEngineByName("vcr").(vcr.VCR) - _, err := vcrService.Issuer().Issue(audit.TestContext(), credential, true, false) + _, err := vcrService.Issuer().Issue(audit.TestContext(), credential, issuer.CredentialOptions{ + Publish: true, + Public: false, + }) assert.ErrorContains(t, err, "unable to publish the issued credential") }) diff --git a/vcr/verifier/verifier.go b/vcr/verifier/verifier.go index 3cb4ec938b..c76887c7ad 100644 --- a/vcr/verifier/verifier.go +++ b/vcr/verifier/verifier.go @@ -19,9 +19,13 @@ package verifier import ( + crypt "crypto" "encoding/json" "errors" "fmt" + "github.com/lestrrat-go/jwx/jwt" + "github.com/nuts-foundation/nuts-node/crypto" + "github.com/nuts-foundation/nuts-node/vcr/issuer" "github.com/nuts-foundation/nuts-node/vdr/resolver" "strings" "time" @@ -43,6 +47,8 @@ const ( maxSkew = 5 * time.Second ) +var errVerificationMethodNotOfIssuer = errors.New("verification method is not of issuer") + // verifier implements the Verifier interface. // It implements the generic methods for verifying verifiable credentials and verifiable presentations. // It does not know anything about the semantics of a credential. It should support a wide range of types. @@ -111,6 +117,17 @@ func (v *verifier) Validate(credentialToVerify vc.VerifiableCredential, at *time return err } + switch credentialToVerify.Format() { + case issuer.JSONLDCredentialFormat: + return v.validateJSONLDCredential(credentialToVerify, at) + case issuer.JWTCredentialFormat: + return v.validateJWTCredential(credentialToVerify, at) + default: + return errors.New("unsupported credential proof format") + } +} + +func (v *verifier) validateJSONLDCredential(credentialToVerify vc.VerifiableCredential, at *time.Time) error { signedDocument, err := proof.NewSignedDocument(credentialToVerify) if err != nil { return fmt.Errorf("unable to build signed document from verifiable credential: %w", err) @@ -124,7 +141,7 @@ func (v *verifier) Validate(credentialToVerify vc.VerifiableCredential, at *time verificationMethod := ldProof.VerificationMethod.String() verificationMethodIssuer := strings.Split(verificationMethod, "#")[0] if verificationMethodIssuer == "" || verificationMethodIssuer != credentialToVerify.Issuer.String() { - return errors.New("verification method is not of issuer") + return errVerificationMethodNotOfIssuer } // find key @@ -140,6 +157,38 @@ func (v *verifier) Validate(credentialToVerify vc.VerifiableCredential, at *time return ldProof.Verify(signedDocument.DocumentWithoutProof(), signature.JSONWebSignature2020{ContextLoader: v.jsonldManager.DocumentLoader()}, pk) } +func (v *verifier) validateJWTCredential(credential vc.VerifiableCredential, at *time.Time) error { + var keyID string + _, err := crypto.ParseJWT(credential.Raw(), func(kid string) (crypt.PublicKey, error) { + keyID = kid + return v.resolveSigningKey(kid, credential.Issuer.String(), at) + }, jwt.WithClock(jwt.ClockFunc(func() time.Time { + if at == nil { + return time.Now() + } + return *at + }))) + if err != nil { + return fmt.Errorf("unable to validate JWT credential: %w", err) + } + if keyID != "" && strings.Split(keyID, "#")[0] != credential.Issuer.String() { + return errVerificationMethodNotOfIssuer + } + return nil +} + +func (v *verifier) resolveSigningKey(kid string, issuer string, at *time.Time) (crypt.PublicKey, error) { + // Compatibility: VC data model v1 puts key discovery out of scope and does not require the `kid` header. + // When `kid` isn't present use the JWT issuer as `kid`, then it is at least compatible with DID methods that contain a single verification method (did:jwk). + if kid == "" { + kid = issuer + } + if strings.HasPrefix(kid, "did:jwk:") && !strings.Contains(kid, "#") { + kid += "#0" + } + return v.keyResolver.ResolveKeyByID(kid, at, resolver.NutsSigningKeyType) +} + // Verify implements the verify interface. // It currently checks if the credential has the required fields and values, if it is valid at the given time and optional the signature. func (v verifier) Verify(credentialToVerify vc.VerifiableCredential, allowUntrusted bool, checkSignature bool, validAt *time.Time) error { @@ -236,7 +285,7 @@ func (v *verifier) RegisterRevocation(revocation credential.Revocation) error { vm := revocation.Proof.VerificationMethod.String() vmIssuer := strings.Split(vm, "#")[0] if vmIssuer != revocation.Issuer.String() { - return errors.New("verificationMethod should owned by the issuer") + return errVerificationMethodNotOfIssuer } pk, err := v.keyResolver.ResolveKeyByID(revocation.Proof.VerificationMethod.String(), &revocation.Date, resolver.NutsSigningKeyType) @@ -264,48 +313,91 @@ func (v verifier) VerifyVP(vp vc.VerifiablePresentation, verifyVCs bool, allowUn } // doVerifyVP delegates VC verification to the supplied Verifier, to aid unit testing. -func (v verifier) doVerifyVP(vcVerifier Verifier, vp vc.VerifiablePresentation, verifyVCs bool, allowUntrustedVCs bool, validAt *time.Time) ([]vc.VerifiableCredential, error) { +func (v verifier) doVerifyVP(vcVerifier Verifier, presentation vc.VerifiablePresentation, verifyVCs bool, allowUntrustedVCs bool, validAt *time.Time) ([]vc.VerifiableCredential, error) { + var err error + switch presentation.Format() { + case issuer.JSONLDPresentationFormat: + err = v.validateJSONLDPresentation(presentation, validAt) + case issuer.JWTPresentationFormat: + err = v.validateJWTPresentation(presentation, validAt) + default: + err = errors.New("unsupported presentation proof format") + } + if err != nil { + return nil, err + } + + if verifyVCs { + for _, current := range presentation.VerifiableCredential { + err := vcVerifier.Verify(current, allowUntrustedVCs, true, validAt) + if err != nil { + return nil, newVerificationError("invalid VC (id=%s): %w", current.ID, err) + } + } + } + + return presentation.VerifiableCredential, nil +} + +func (v *verifier) validateJSONLDPresentation(presentation vc.VerifiablePresentation, validAt *time.Time) error { // Multiple proofs might be supported in the future, when there's an actual use case. - if len(vp.Proof) != 1 { - return nil, newVerificationError("exactly 1 proof is expected") + if len(presentation.Proof) != 1 { + return newVerificationError("exactly 1 proof is expected") } // Make sure the proofs are LD-proofs var ldProofs []proof.LDProof - err := vp.UnmarshalProofValue(&ldProofs) + err := presentation.UnmarshalProofValue(&ldProofs) if err != nil { - return nil, newVerificationError("unsupported proof type: %w", err) + return newVerificationError("unsupported proof type: %w", err) } ldProof := ldProofs[0] // Validate signing time if !v.validateAtTime(ldProof.Created, ldProof.Expires, validAt) { - return nil, toVerificationError(types.ErrPresentationNotValidAtTime) + return toVerificationError(types.ErrPresentationNotValidAtTime) } // Validate signature signingKey, err := v.keyResolver.ResolveKeyByID(ldProof.VerificationMethod.String(), validAt, resolver.NutsSigningKeyType) if err != nil { - return nil, fmt.Errorf("unable to resolve valid signing key: %w", err) + return fmt.Errorf("unable to resolve valid signing key: %w", err) } - signedDocument, err := proof.NewSignedDocument(vp) + signedDocument, err := proof.NewSignedDocument(presentation) if err != nil { - return nil, newVerificationError("invalid LD-JSON document: %w", err) + return newVerificationError("invalid LD-JSON document: %w", err) } err = ldProof.Verify(signedDocument.DocumentWithoutProof(), signature.JSONWebSignature2020{ContextLoader: v.jsonldManager.DocumentLoader()}, signingKey) if err != nil { - return nil, newVerificationError("invalid signature: %w", err) + return newVerificationError("invalid signature: %w", err) } + return nil +} - if verifyVCs { - for _, current := range vp.VerifiableCredential { - err := vcVerifier.Verify(current, allowUntrustedVCs, true, validAt) - if err != nil { - return nil, newVerificationError("invalid VC (id=%s): %w", current.ID, err) - } +func (v *verifier) validateJWTPresentation(presentation vc.VerifiablePresentation, at *time.Time) error { + var keyID string + if len(presentation.VerifiableCredential) != 1 { + return errors.New("exactly 1 credential in JWT VP is expected") + } + subjectDID, err := presentation.VerifiableCredential[0].SubjectDID() + if err != nil { + return err + } + _, err = crypto.ParseJWT(presentation.Raw(), func(kid string) (crypt.PublicKey, error) { + keyID = kid + return v.resolveSigningKey(kid, subjectDID.String(), at) + }, jwt.WithClock(jwt.ClockFunc(func() time.Time { + if at == nil { + return time.Now() } + return *at + }))) + if err != nil { + return fmt.Errorf("unable to validate JWT credential: %w", err) } - - return vp.VerifiableCredential, nil + if keyID != "" && strings.Split(keyID, "#")[0] != subjectDID.String() { + return errVerificationMethodNotOfIssuer + } + return nil } func (v *verifier) validateType(credential vc.VerifiableCredential) error { diff --git a/vcr/verifier/verifier_test.go b/vcr/verifier/verifier_test.go index 841d5ee383..3adb1d33ba 100644 --- a/vcr/verifier/verifier_test.go +++ b/vcr/verifier/verifier_test.go @@ -19,10 +19,18 @@ package verifier import ( + "context" + crypt "crypto" "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "encoding/base64" "encoding/json" "errors" + "github.com/lestrrat-go/jwx/jwk" + "github.com/nuts-foundation/nuts-node/audit" "github.com/nuts-foundation/nuts-node/crypto/storage/spi" + "github.com/nuts-foundation/nuts-node/vdr/didjwk" "github.com/nuts-foundation/nuts-node/vdr/resolver" "github.com/stretchr/testify/require" "os" @@ -70,7 +78,7 @@ func Test_verifier_Validate(t *testing.T) { timeFunc = time.Now }() - t.Run("ok", func(t *testing.T) { + t.Run("JSON-LD", func(t *testing.T) { ctx := newMockContext(t) instance := ctx.verifier @@ -80,6 +88,85 @@ func Test_verifier_Validate(t *testing.T) { assert.NoError(t, err) }) + t.Run("JWT", func(t *testing.T) { + // Create did:jwk for issuer, and sign credential + keyStore := crypto.NewMemoryCryptoInstance() + key, err := keyStore.New(audit.TestContext(), func(key crypt.PublicKey) (string, error) { + keyAsJWK, _ := jwk.New(key) + keyJSON, _ := json.Marshal(keyAsJWK) + return "did:jwk:" + base64.RawStdEncoding.EncodeToString(keyJSON) + "#0", nil + }) + require.NoError(t, err) + + template := testCredential(t) + template.Issuer = did.MustParseDIDURL(key.KID()).WithoutURL().URI() + + cred, err := vc.CreateJWTVerifiableCredential(audit.TestContext(), template, func(ctx context.Context, claims map[string]interface{}, headers map[string]interface{}) (string, error) { + return keyStore.SignJWT(ctx, claims, headers, key) + }) + require.NoError(t, err) + + t.Run("with kid header", func(t *testing.T) { + ctx := newMockContext(t) + instance := ctx.verifier + + ctx.keyResolver.EXPECT().ResolveKeyByID(key.KID(), gomock.Any(), resolver.NutsSigningKeyType).Return(key.Public(), nil) + err = instance.Validate(*cred, nil) + + assert.NoError(t, err) + }) + t.Run("kid header does not match credential issuer", func(t *testing.T) { + ctx := newMockContext(t) + instance := ctx.verifier + + cred, err := vc.CreateJWTVerifiableCredential(audit.TestContext(), template, func(ctx context.Context, claims map[string]interface{}, headers map[string]interface{}) (string, error) { + return keyStore.SignJWT(ctx, claims, headers, key) + }) + require.NoError(t, err) + cred.Issuer = ssi.MustParseURI("did:example:test") + + ctx.keyResolver.EXPECT().ResolveKeyByID(key.KID(), gomock.Any(), resolver.NutsSigningKeyType).Return(key.Public(), nil) + err = instance.Validate(*cred, nil) + + assert.ErrorIs(t, err, errVerificationMethodNotOfIssuer) + }) + t.Run("signature invalid", func(t *testing.T) { + ctx := newMockContext(t) + instance := ctx.verifier + + realKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + + ctx.keyResolver.EXPECT().ResolveKeyByID(key.KID(), gomock.Any(), resolver.NutsSigningKeyType).Return(realKey.Public(), nil) + err = instance.Validate(*cred, nil) + + assert.EqualError(t, err, "unable to validate JWT credential: failed to verify jws signature: failed to verify message: failed to verify signature using ecdsa") + }) + t.Run("expired token", func(t *testing.T) { + // Credential taken from Sphereon Wallet, expires on Tue Oct 03 2023 + const credentialJSON = `eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2OTYzMDE3MDgsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJHdWVzdENyZWRlbnRpYWwiXSwiY3JlZGVudGlhbFN1YmplY3QiOnsiZmlyc3ROYW1lIjoiSGVsbG8iLCJsYXN0TmFtZSI6IlNwaGVyZW9uIiwiZW1haWwiOiJzcGhlcmVvbkBleGFtcGxlLmNvbSIsInR5cGUiOiJTcGhlcmVvbiBHdWVzdCIsImlkIjoiZGlkOmp3azpleUpoYkdjaU9pSkZVekkxTmtzaUxDSjFjMlVpT2lKemFXY2lMQ0pyZEhraU9pSkZReUlzSW1OeWRpSTZJbk5sWTNBeU5UWnJNU0lzSW5naU9pSmpNVmRZY3pkWE0yMTVjMlZWWms1Q2NYTjRaRkJYUWtsSGFFdGtORlI2TUV4U0xVWnFPRVpOV1dFd0lpd2llU0k2SWxkdGEwTllkVEYzZVhwYVowZE9OMVY0VG1Gd2NIRnVUMUZoVDJ0WE1rTm5UMU51VDI5NVRVbFVkV01pZlEifX0sIkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJHdWVzdENyZWRlbnRpYWwiXSwiZXhwaXJhdGlvbkRhdGUiOiIyMDIzLTEwLTAzVDAyOjU1OjA4LjEzM1oiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJmaXJzdE5hbWUiOiJIZWxsbyIsImxhc3ROYW1lIjoiU3BoZXJlb24iLCJlbWFpbCI6InNwaGVyZW9uQGV4YW1wbGUuY29tIiwidHlwZSI6IlNwaGVyZW9uIEd1ZXN0IiwiaWQiOiJkaWQ6andrOmV5SmhiR2NpT2lKRlV6STFOa3NpTENKMWMyVWlPaUp6YVdjaUxDSnJkSGtpT2lKRlF5SXNJbU55ZGlJNkluTmxZM0F5TlRack1TSXNJbmdpT2lKak1WZFljemRYTTIxNWMyVlZaazVDY1hONFpGQlhRa2xIYUV0a05GUjZNRXhTTFVacU9FWk5XV0V3SWl3aWVTSTZJbGR0YTBOWWRURjNlWHBhWjBkT04xVjRUbUZ3Y0hGdVQxRmhUMnRYTWtOblQxTnVUMjk1VFVsVWRXTWlmUSJ9LCJpc3N1ZXIiOiJkaWQ6andrOmV5SmhiR2NpT2lKRlV6STFOaUlzSW5WelpTSTZJbk5wWnlJc0ltdDBlU0k2SWtWRElpd2lZM0oySWpvaVVDMHlOVFlpTENKNElqb2lWRWN5U0RKNE1tUlhXRTR6ZFVOeFduQnhSakY1YzBGUVVWWkVTa1ZPWDBndFEwMTBZbWRxWWkxT1p5SXNJbmtpT2lJNVRUaE9lR1F3VUU0eU1rMDViRkJFZUdSd1JIQnZWRXg2TVRWM1pubGFTbk0yV21oTFNWVktNek00SW4wIiwiaXNzdWFuY2VEYXRlIjoiMjAyMy0wOS0yOVQxMjozMTowOC4xMzNaIiwic3ViIjoiZGlkOmp3azpleUpoYkdjaU9pSkZVekkxTmtzaUxDSjFjMlVpT2lKemFXY2lMQ0pyZEhraU9pSkZReUlzSW1OeWRpSTZJbk5sWTNBeU5UWnJNU0lzSW5naU9pSmpNVmRZY3pkWE0yMTVjMlZWWms1Q2NYTjRaRkJYUWtsSGFFdGtORlI2TUV4U0xVWnFPRVpOV1dFd0lpd2llU0k2SWxkdGEwTllkVEYzZVhwYVowZE9OMVY0VG1Gd2NIRnVUMUZoVDJ0WE1rTm5UMU51VDI5NVRVbFVkV01pZlEiLCJuYmYiOjE2OTU5OTA2NjgsImlzcyI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGVXpJMU5pSXNJblZ6WlNJNkluTnBaeUlzSW10MGVTSTZJa1ZESWl3aVkzSjJJam9pVUMweU5UWWlMQ0o0SWpvaVZFY3lTREo0TW1SWFdFNHpkVU54V25CeFJqRjVjMEZRVVZaRVNrVk9YMGd0UTAxMFltZHFZaTFPWnlJc0lua2lPaUk1VFRoT2VHUXdVRTR5TWswNWJGQkVlR1J3UkhCdlZFeDZNVFYzWm5sYVNuTTJXbWhMU1ZWS016TTRJbjAifQ.wdhtLXE4jU1C-3YBBpP9-qE-yh1xOZ6lBLJ-0e5_Sa7fnrUHcAaU1n3kN2CeCyTVjtm1Uy3Tl6RzUOM6MjP3vQ` + cred, _ := vc.ParseVerifiableCredential(credentialJSON) + + keyResolver := resolver.DIDKeyResolver{ + Resolver: didjwk.NewResolver(), + } + err := (&verifier{keyResolver: keyResolver}).Validate(*cred, nil) + + assert.EqualError(t, err, "unable to validate JWT credential: exp not satisfied") + }) + t.Run("without kid header, derived from issuer", func(t *testing.T) { + // Credential taken from Sphereon Wallet + const credentialJSON = `eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2OTYzMDE3MDgsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJHdWVzdENyZWRlbnRpYWwiXSwiY3JlZGVudGlhbFN1YmplY3QiOnsiZmlyc3ROYW1lIjoiSGVsbG8iLCJsYXN0TmFtZSI6IlNwaGVyZW9uIiwiZW1haWwiOiJzcGhlcmVvbkBleGFtcGxlLmNvbSIsInR5cGUiOiJTcGhlcmVvbiBHdWVzdCIsImlkIjoiZGlkOmp3azpleUpoYkdjaU9pSkZVekkxTmtzaUxDSjFjMlVpT2lKemFXY2lMQ0pyZEhraU9pSkZReUlzSW1OeWRpSTZJbk5sWTNBeU5UWnJNU0lzSW5naU9pSmpNVmRZY3pkWE0yMTVjMlZWWms1Q2NYTjRaRkJYUWtsSGFFdGtORlI2TUV4U0xVWnFPRVpOV1dFd0lpd2llU0k2SWxkdGEwTllkVEYzZVhwYVowZE9OMVY0VG1Gd2NIRnVUMUZoVDJ0WE1rTm5UMU51VDI5NVRVbFVkV01pZlEifX0sIkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJHdWVzdENyZWRlbnRpYWwiXSwiZXhwaXJhdGlvbkRhdGUiOiIyMDIzLTEwLTAzVDAyOjU1OjA4LjEzM1oiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJmaXJzdE5hbWUiOiJIZWxsbyIsImxhc3ROYW1lIjoiU3BoZXJlb24iLCJlbWFpbCI6InNwaGVyZW9uQGV4YW1wbGUuY29tIiwidHlwZSI6IlNwaGVyZW9uIEd1ZXN0IiwiaWQiOiJkaWQ6andrOmV5SmhiR2NpT2lKRlV6STFOa3NpTENKMWMyVWlPaUp6YVdjaUxDSnJkSGtpT2lKRlF5SXNJbU55ZGlJNkluTmxZM0F5TlRack1TSXNJbmdpT2lKak1WZFljemRYTTIxNWMyVlZaazVDY1hONFpGQlhRa2xIYUV0a05GUjZNRXhTTFVacU9FWk5XV0V3SWl3aWVTSTZJbGR0YTBOWWRURjNlWHBhWjBkT04xVjRUbUZ3Y0hGdVQxRmhUMnRYTWtOblQxTnVUMjk1VFVsVWRXTWlmUSJ9LCJpc3N1ZXIiOiJkaWQ6andrOmV5SmhiR2NpT2lKRlV6STFOaUlzSW5WelpTSTZJbk5wWnlJc0ltdDBlU0k2SWtWRElpd2lZM0oySWpvaVVDMHlOVFlpTENKNElqb2lWRWN5U0RKNE1tUlhXRTR6ZFVOeFduQnhSakY1YzBGUVVWWkVTa1ZPWDBndFEwMTBZbWRxWWkxT1p5SXNJbmtpT2lJNVRUaE9lR1F3VUU0eU1rMDViRkJFZUdSd1JIQnZWRXg2TVRWM1pubGFTbk0yV21oTFNWVktNek00SW4wIiwiaXNzdWFuY2VEYXRlIjoiMjAyMy0wOS0yOVQxMjozMTowOC4xMzNaIiwic3ViIjoiZGlkOmp3azpleUpoYkdjaU9pSkZVekkxTmtzaUxDSjFjMlVpT2lKemFXY2lMQ0pyZEhraU9pSkZReUlzSW1OeWRpSTZJbk5sWTNBeU5UWnJNU0lzSW5naU9pSmpNVmRZY3pkWE0yMTVjMlZWWms1Q2NYTjRaRkJYUWtsSGFFdGtORlI2TUV4U0xVWnFPRVpOV1dFd0lpd2llU0k2SWxkdGEwTllkVEYzZVhwYVowZE9OMVY0VG1Gd2NIRnVUMUZoVDJ0WE1rTm5UMU51VDI5NVRVbFVkV01pZlEiLCJuYmYiOjE2OTU5OTA2NjgsImlzcyI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGVXpJMU5pSXNJblZ6WlNJNkluTnBaeUlzSW10MGVTSTZJa1ZESWl3aVkzSjJJam9pVUMweU5UWWlMQ0o0SWpvaVZFY3lTREo0TW1SWFdFNHpkVU54V25CeFJqRjVjMEZRVVZaRVNrVk9YMGd0UTAxMFltZHFZaTFPWnlJc0lua2lPaUk1VFRoT2VHUXdVRTR5TWswNWJGQkVlR1J3UkhCdlZFeDZNVFYzWm5sYVNuTTJXbWhMU1ZWS016TTRJbjAifQ.wdhtLXE4jU1C-3YBBpP9-qE-yh1xOZ6lBLJ-0e5_Sa7fnrUHcAaU1n3kN2CeCyTVjtm1Uy3Tl6RzUOM6MjP3vQ` + cred, _ := vc.ParseVerifiableCredential(credentialJSON) + + keyResolver := resolver.DIDKeyResolver{ + Resolver: didjwk.NewResolver(), + } + validAt := time.Date(2023, 9, 30, 0, 0, 0, 0, time.UTC) + err := (&verifier{keyResolver: keyResolver}).Validate(*cred, &validAt) + + assert.NoError(t, err) + }) + }) t.Run("type", func(t *testing.T) { t.Run("incorrect number of types", func(t *testing.T) { @@ -114,7 +201,7 @@ func Test_verifier_Validate(t *testing.T) { err := instance.Validate(vc2, nil) assert.Error(t, err) - assert.EqualError(t, err, "verification method is not of issuer") + assert.ErrorIs(t, err, errVerificationMethodNotOfIssuer) }) t.Run("error - wrong hashed payload", func(t *testing.T) { @@ -438,7 +525,7 @@ func Test_verifier_CheckAndStoreRevocation(t *testing.T) { assert.NoError(t, json.Unmarshal(rawRevocation, &revocation)) revocation.Proof.VerificationMethod = ssi.MustParseURI("did:nuts:123#abc") err := sut.verifier.RegisterRevocation(revocation) - assert.EqualError(t, err, "verificationMethod should owned by the issuer") + assert.ErrorIs(t, err, errVerificationMethodNotOfIssuer) }) t.Run("it fails when the revoked credential and revocation-issuer are not from the same identity", func(t *testing.T) { @@ -484,7 +571,72 @@ func Test_verifier_CheckAndStoreRevocation(t *testing.T) { } func TestVerifier_VerifyVP(t *testing.T) { - rawVP := `{ + t.Run("JWT", func(t *testing.T) { + const keyID = "did:nuts:GvkzxsezHvEc8nGhgz6Xo3jbqkHwswLmWw3CYtCm7hAW#abc-method-1" + key, err := jwk.ParseKey([]byte(`{ + "crv": "P-256", + "d": "mvipTdytRXwTTY_6wJl5Cwj0YQ4-QdJK-fEC8DzL9_M", + "kty": "EC", + "x": "8WvKOR7ZpOSfNxT20Qig8DuVY7QAwx6Qe4NNejTN3po", + "y": "UYZoXK13bedMDHvsrGskxihDuWIXgGBdQfTvjyQlCDE" +}`)) + require.NoError(t, err) + var publicKey crypt.PublicKey + require.NoError(t, key.Raw(&publicKey)) + + const rawVP = `eyJhbGciOiJFUzI1NiIsImtpZCI6ImRpZDpudXRzOkd2a3p4c2V6SHZFYzhuR2hnejZYbzNqYnFrSHdzd0xtV3czQ1l0Q203aEFXI2FiYy1tZXRob2QtMSIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2OTc2OTY3NDEsImlzcyI6ImRpZDpudXRzOkd2a3p4c2V6SHZFYzhuR2hnejZYbzNqYnFrSHdzd0xtV3czQ1l0Q203aEFXIiwibmJmIjoxNjk3NjEwMzQxLCJzdWIiOiJkaWQ6bnV0czpHdmt6eHNlekh2RWM4bkdoZ3o2WG8zamJxa0h3c3dMbVd3M0NZdENtN2hBVyIsInZwIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sInR5cGUiOiJWZXJpZmlhYmxlUHJlc2VudGF0aW9uIiwidmVyaWZpYWJsZUNyZWRlbnRpYWwiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJodHRwczovL251dHMubmwvY3JlZGVudGlhbHMvdjEiLCJodHRwczovL3czYy1jY2cuZ2l0aHViLmlvL2xkcy1qd3MyMDIwL2NvbnRleHRzL2xkcy1qd3MyMDIwLXYxLmpzb24iXSwiY3JlZGVudGlhbFN1YmplY3QiOnsiY29tcGFueSI6eyJjaXR5IjoiSGVuZ2VsbyIsIm5hbWUiOiJEZSBiZXN0ZSB6b3JnIn0sImlkIjoiZGlkOm51dHM6R3ZrenhzZXpIdkVjOG5HaGd6NlhvM2picWtId3N3TG1XdzNDWXRDbTdoQVcifSwiaWQiOiJkaWQ6bnV0czo0dHpNYVdmcGl6VktlQThmc2NDM0pUZFdCYzNhc1VXV01qNWhVRkhkV1gzSCNmNDNiZWY0Zi0xYTc5LTQzNjQtOTJmMy0zZmM3NDNmYTlmMTkiLCJpc3N1YW5jZURhdGUiOiIyMDIxLTEyLTI0VDEzOjIxOjI5LjA4NzIwNSswMTowMCIsImlzc3VlciI6ImRpZDpudXRzOjR0ek1hV2ZwaXpWS2VBOGZzY0MzSlRkV0JjM2FzVVdXTWo1aFVGSGRXWDNIIiwicHJvb2YiOnsiY3JlYXRlZCI6IjIwMjEtMTItMjRUMTM6MjE6MjkuMDg3MjA1KzAxOjAwIiwiandzIjoiZXlKaGJHY2lPaUpGVXpJMU5pSXNJbUkyTkNJNlptRnNjMlVzSW1OeWFYUWlPbHNpWWpZMElsMTkuLmhQTTJHTGMxSzlkMkQ4U2J2ZTAwNHg5U3VtakxxYVhUaldoVWh2cVdSd3hmUldsd2ZwNWdIRFVZdVJvRWpoQ1hmTHQtX3Uta25DaFZtSzk4ME4zTEJ3IiwicHJvb2ZQdXJwb3NlIjoiTnV0c1NpZ25pbmdLZXlUeXBlIiwidHlwZSI6Ikpzb25XZWJTaWduYXR1cmUyMDIwIiwidmVyaWZpY2F0aW9uTWV0aG9kIjoiZGlkOm51dHM6R3ZrenhzZXpIdkVjOG5HaGd6NlhvM2picWtId3N3TG1XdzNDWXRDbTdoQVcjYWJjLW1ldGhvZC0xIn0sInR5cGUiOlsiQ29tcGFueUNyZWRlbnRpYWwiLCJWZXJpZmlhYmxlQ3JlZGVudGlhbCJdfX19.v3beJvGa3HeImU3VLvsrZjnHs0krKPaCdTEh-qHS7j26LIQYcMHhrLkIexrpPO5z0TKSDnKq5Jl10SWaJpLRIA` + + presentation, err := vc.ParseVerifiablePresentation(rawVP) + require.NoError(t, err) + + t.Run("ok", func(t *testing.T) { + ctx := newMockContext(t) + ctx.keyResolver.EXPECT().ResolveKeyByID(keyID, gomock.Any(), resolver.NutsSigningKeyType).Return(publicKey, nil) + + validAt := time.Date(2023, 10, 18, 12, 0, 0, 0, time.UTC) + vcs, err := ctx.verifier.VerifyVP(*presentation, false, false, &validAt) + + assert.NoError(t, err) + assert.Len(t, vcs, 1) + }) + t.Run("JWT expired", func(t *testing.T) { + ctx := newMockContext(t) + ctx.keyResolver.EXPECT().ResolveKeyByID(keyID, gomock.Any(), resolver.NutsSigningKeyType).Return(publicKey, nil) + + 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, "unable to validate JWT credential: exp not satisfied") + assert.Empty(t, vcs) + }) + t.Run("VP signer != VC credentialSubject.id", func(t *testing.T) { + // This VP was produced by a Sphereon Wallet, using did:key. The signer of the VP is a did:key, + // but the holder of the contained credential is a did:jwt. So the presenter is not the holder. Weird? + const rawVP = `eyJraWQiOiJkaWQ6a2V5Ono2TWtzRXl4NmQ1cEIxZWtvYVZtYUdzaWJiY1lIRTlWeHg3VjEzUFNxUHd4WVJ6TCN6Nk1rc0V5eDZkNXBCMWVrb2FWbWFHc2liYmNZSEU5Vnh4N1YxM1BTcVB3eFlSekwiLCJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJ2cCI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vaWRlbnRpdHkuZm91bmRhdGlvbi9wcmVzZW50YXRpb24tZXhjaGFuZ2Uvc3VibWlzc2lvbi92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVQcmVzZW50YXRpb24iLCJQcmVzZW50YXRpb25TdWJtaXNzaW9uIl0sInZlcmlmaWFibGVDcmVkZW50aWFsIjpbImV5SmhiR2NpT2lKRlV6STFOaUlzSW5SNWNDSTZJa3BYVkNKOS5leUpsZUhBaU9qRTJPVFl6TURFM01EZ3NJblpqSWpwN0lrQmpiMjUwWlhoMElqcGJJbWgwZEhCek9pOHZkM2QzTG5jekxtOXlaeTh5TURFNEwyTnlaV1JsYm5ScFlXeHpMM1l4SWwwc0luUjVjR1VpT2xzaVZtVnlhV1pwWVdKc1pVTnlaV1JsYm5ScFlXd2lMQ0pIZFdWemRFTnlaV1JsYm5ScFlXd2lYU3dpWTNKbFpHVnVkR2xoYkZOMVltcGxZM1FpT25zaVptbHljM1JPWVcxbElqb2lTR1ZzYkc4aUxDSnNZWE4wVG1GdFpTSTZJbE53YUdWeVpXOXVJaXdpWlcxaGFXd2lPaUp6Y0dobGNtVnZia0JsZUdGdGNHeGxMbU52YlNJc0luUjVjR1VpT2lKVGNHaGxjbVZ2YmlCSGRXVnpkQ0lzSW1sa0lqb2laR2xrT21wM2F6cGxlVXBvWWtkamFVOXBTa1pWZWtreFRtdHphVXhEU2pGak1sVnBUMmxLZW1GWFkybE1RMHB5WkVocmFVOXBTa1pSZVVselNXMU9lV1JwU1RaSmJrNXNXVE5CZVU1VVduSk5VMGx6U1c1bmFVOXBTbXBOVm1SWlkzcGtXRTB5TVRWak1sWldXbXMxUTJOWVRqUmFSa0pZVVd0c1NHRkZkR3RPUmxJMlRVVjRVMHhWV25GUFJWcE9WMWRGZDBscGQybGxVMGsyU1d4a2RHRXdUbGxrVkVZelpWaHdZVm93WkU5T01WWTBWRzFHZDJOSVJuVlVNVVpvVkRKMFdFMXJUbTVVTVU1MVZESTVOVlJWYkZWa1YwMXBabEVpZlgwc0lrQmpiMjUwWlhoMElqcGJJbWgwZEhCek9pOHZkM2QzTG5jekxtOXlaeTh5TURFNEwyTnlaV1JsYm5ScFlXeHpMM1l4SWwwc0luUjVjR1VpT2xzaVZtVnlhV1pwWVdKc1pVTnlaV1JsYm5ScFlXd2lMQ0pIZFdWemRFTnlaV1JsYm5ScFlXd2lYU3dpWlhod2FYSmhkR2x2YmtSaGRHVWlPaUl5TURJekxURXdMVEF6VkRBeU9qVTFPakE0TGpFek0xb2lMQ0pqY21Wa1pXNTBhV0ZzVTNWaWFtVmpkQ0k2ZXlKbWFYSnpkRTVoYldVaU9pSklaV3hzYnlJc0lteGhjM1JPWVcxbElqb2lVM0JvWlhKbGIyNGlMQ0psYldGcGJDSTZJbk53YUdWeVpXOXVRR1Y0WVcxd2JHVXVZMjl0SWl3aWRIbHdaU0k2SWxOd2FHVnlaVzl1SUVkMVpYTjBJaXdpYVdRaU9pSmthV1E2YW5kck9tVjVTbWhpUjJOcFQybEtSbFY2U1RGT2EzTnBURU5LTVdNeVZXbFBhVXA2WVZkamFVeERTbkprU0d0cFQybEtSbEY1U1hOSmJVNTVaR2xKTmtsdVRteFpNMEY1VGxSYWNrMVRTWE5KYm1kcFQybEthazFXWkZsamVtUllUVEl4TldNeVZsWmFhelZEWTFoT05GcEdRbGhSYTJ4SVlVVjBhMDVHVWpaTlJYaFRURlZhY1U5RldrNVhWMFYzU1dsM2FXVlRTVFpKYkdSMFlUQk9XV1JVUmpObFdIQmhXakJrVDA0eFZqUlViVVozWTBoR2RWUXhSbWhVTW5SWVRXdE9ibFF4VG5WVU1qazFWRlZzVldSWFRXbG1VU0o5TENKcGMzTjFaWElpT2lKa2FXUTZhbmRyT21WNVNtaGlSMk5wVDJsS1JsVjZTVEZPYVVselNXNVdlbHBUU1RaSmJrNXdXbmxKYzBsdGREQmxVMGsyU1d0V1JFbHBkMmxaTTBveVNXcHZhVlZETUhsT1ZGbHBURU5LTkVscWIybFdSV041VTBSS05FMXRVbGhYUlRSNlpGVk9lRmR1UW5oU2FrWTFZekJHVVZWV1drVlRhMVpQV0RCbmRGRXdNVEJaYldSeFdXa3hUMXA1U1hOSmJtdHBUMmxKTlZSVWFFOWxSMUYzVlVVMGVVMXJNRFZpUmtKRlpVZFNkMUpJUW5aV1JYZzJUVlJXTTFwdWJHRlRiazB5VjIxb1RGTldWa3ROZWswMFNXNHdJaXdpYVhOemRXRnVZMlZFWVhSbElqb2lNakF5TXkwd09TMHlPVlF4TWpvek1Ub3dPQzR4TXpOYUlpd2ljM1ZpSWpvaVpHbGtPbXAzYXpwbGVVcG9Za2RqYVU5cFNrWlZla2t4VG10emFVeERTakZqTWxWcFQybEtlbUZYWTJsTVEwcHlaRWhyYVU5cFNrWlJlVWx6U1cxT2VXUnBTVFpKYms1c1dUTkJlVTVVV25KTlUwbHpTVzVuYVU5cFNtcE5WbVJaWTNwa1dFMHlNVFZqTWxaV1dtczFRMk5ZVGpSYVJrSllVV3RzU0dGRmRHdE9SbEkyVFVWNFUweFZXbkZQUlZwT1YxZEZkMGxwZDJsbFUwazJTV3hrZEdFd1RsbGtWRVl6WlZod1lWb3daRTlPTVZZMFZHMUdkMk5JUm5WVU1VWm9WREowV0UxclRtNVVNVTUxVkRJNU5WUlZiRlZrVjAxcFpsRWlMQ0p1WW1ZaU9qRTJPVFU1T1RBMk5qZ3NJbWx6Y3lJNkltUnBaRHBxZDJzNlpYbEthR0pIWTJsUGFVcEdWWHBKTVU1cFNYTkpibFo2V2xOSk5rbHVUbkJhZVVselNXMTBNR1ZUU1RaSmExWkVTV2wzYVZrelNqSkphbTlwVlVNd2VVNVVXV2xNUTBvMFNXcHZhVlpGWTNsVFJFbzBUVzFTV0ZkRk5IcGtWVTU0VjI1Q2VGSnFSalZqTUVaUlZWWmFSVk5yVms5WU1HZDBVVEF4TUZsdFpIRlphVEZQV25sSmMwbHVhMmxQYVVrMVZGUm9UMlZIVVhkVlJUUjVUV3N3TldKR1FrVmxSMUozVWtoQ2RsWkZlRFpOVkZZeldtNXNZVk51VFRKWGJXaE1VMVpXUzAxNlRUUkpiakFpZlEud2RodExYRTRqVTFDLTNZQkJwUDktcUUteWgxeE9aNmxCTEotMGU1X1NhN2ZuclVIY0FhVTFuM2tOMkNlQ3lUVmp0bTFVeTNUbDZSelVPTTZNalAzdlEiXX0sInByZXNlbnRhdGlvbl9zdWJtaXNzaW9uIjp7ImlkIjoidG9DdGp5Y0V3QlZCWVBsbktBQTZGIiwiZGVmaW5pdGlvbl9pZCI6InNwaGVyZW9uIiwiZGVzY3JpcHRvcl9tYXAiOlt7ImlkIjoiNGNlN2FmZjEtMDIzNC00ZjM1LTlkMjEtMjUxNjY4YTYwOTUwIiwiZm9ybWF0Ijoiand0X3ZjIiwicGF0aCI6IiQudmVyaWZpYWJsZUNyZWRlbnRpYWxbMF0ifV19LCJuYmYiOjE2OTU5OTU2MzYsImlzcyI6ImRpZDprZXk6ejZNa3NFeXg2ZDVwQjFla29hVm1hR3NpYmJjWUhFOVZ4eDdWMTNQU3FQd3hZUnpMIn0.w3guHX-pmxJGGn5dGSSIKSba9xywnOutDk-l3tc_bpgHEOSbcR1mmmCqX5sSlZM_G0hgAbgpIv_YYI5iQNIfCw` + const keyID = "did:key:z6MksEyx6d5pB1ekoaVmaGsibbcYHE9Vxx7V13PSqPwxYRzL#z6MksEyx6d5pB1ekoaVmaGsibbcYHE9Vxx7V13PSqPwxYRzL" + keyAsJWK, err := jwk.ParseKey([]byte(`{ + "kty": "OKP", + "crv": "Ed25519", + "x": "vgLDESnU0TIlW-PmajyrvSlk9VysAsRkSYiEPBELj-U" + }`)) + require.NoError(t, err) + require.NoError(t, keyAsJWK.Set("kid", keyID)) + publicKey, err := keyAsJWK.PublicKey() + require.NoError(t, err) + + presentation, err := vc.ParseVerifiablePresentation(rawVP) + require.NoError(t, err) + ctx := newMockContext(t) + ctx.keyResolver.EXPECT().ResolveKeyByID(keyID, gomock.Any(), resolver.NutsSigningKeyType).Return(publicKey, nil) + + vcs, err := ctx.verifier.VerifyVP(*presentation, false, false, nil) + + assert.EqualError(t, err, "verification method is not of issuer") + assert.Empty(t, vcs) + }) + }) + t.Run("JSONLD", func(t *testing.T) { + rawVP := `{ "@context": [ "https://www.w3.org/2018/credentials/v1", "https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json" @@ -525,140 +677,141 @@ func TestVerifier_VerifyVP(t *testing.T) { ] } }` - vp := vc.VerifiablePresentation{} - _ = json.Unmarshal([]byte(rawVP), &vp) - vpSignerKeyID := did.MustParseDIDURL(vp.Proof[0].(map[string]interface{})["verificationMethod"].(string)) - - t.Run("ok - do not verify VCs", func(t *testing.T) { + vp := vc.VerifiablePresentation{} _ = json.Unmarshal([]byte(rawVP), &vp) + vpSignerKeyID := did.MustParseDIDURL(vp.Proof[0].(map[string]interface{})["verificationMethod"].(string)) - var validAt *time.Time + t.Run("ok - do not verify VCs", func(t *testing.T) { + _ = json.Unmarshal([]byte(rawVP), &vp) - ctx := newMockContext(t) - ctx.keyResolver.EXPECT().ResolveKeyByID(vpSignerKeyID.String(), validAt, resolver.NutsSigningKeyType).Return(vdr.TestMethodDIDAPrivateKey().Public(), nil) + var validAt *time.Time - vcs, err := ctx.verifier.VerifyVP(vp, false, false, validAt) + ctx := newMockContext(t) + ctx.keyResolver.EXPECT().ResolveKeyByID(vpSignerKeyID.String(), validAt, resolver.NutsSigningKeyType).Return(vdr.TestMethodDIDAPrivateKey().Public(), nil) - assert.NoError(t, err) - assert.Len(t, vcs, 1) - }) - t.Run("ok - verify VCs (and verify trusted)", func(t *testing.T) { - _ = json.Unmarshal([]byte(rawVP), &vp) + vcs, err := ctx.verifier.VerifyVP(vp, false, false, validAt) - var validAt *time.Time + assert.NoError(t, err) + assert.Len(t, vcs, 1) + }) + t.Run("ok - verify VCs (and verify trusted)", func(t *testing.T) { + _ = json.Unmarshal([]byte(rawVP), &vp) - ctx := newMockContext(t) - ctx.keyResolver.EXPECT().ResolveKeyByID(vpSignerKeyID.String(), validAt, resolver.NutsSigningKeyType).Return(vdr.TestMethodDIDAPrivateKey().Public(), nil) + var validAt *time.Time - mockVerifier := NewMockVerifier(ctx.ctrl) - mockVerifier.EXPECT().Verify(vp.VerifiableCredential[0], false, true, validAt) + ctx := newMockContext(t) + ctx.keyResolver.EXPECT().ResolveKeyByID(vpSignerKeyID.String(), validAt, resolver.NutsSigningKeyType).Return(vdr.TestMethodDIDAPrivateKey().Public(), nil) - vcs, err := ctx.verifier.doVerifyVP(mockVerifier, vp, true, false, validAt) + mockVerifier := NewMockVerifier(ctx.ctrl) + mockVerifier.EXPECT().Verify(vp.VerifiableCredential[0], false, true, validAt) - assert.NoError(t, err) - assert.Len(t, vcs, 1) - }) - t.Run("ok - verify VCs (do not need to be trusted)", func(t *testing.T) { - _ = json.Unmarshal([]byte(rawVP), &vp) + vcs, err := ctx.verifier.doVerifyVP(mockVerifier, vp, true, false, validAt) - var validAt *time.Time + assert.NoError(t, err) + assert.Len(t, vcs, 1) + }) + t.Run("ok - verify VCs (do not need to be trusted)", func(t *testing.T) { + _ = json.Unmarshal([]byte(rawVP), &vp) - ctx := newMockContext(t) - ctx.keyResolver.EXPECT().ResolveKeyByID(vpSignerKeyID.String(), validAt, resolver.NutsSigningKeyType).Return(vdr.TestMethodDIDAPrivateKey().Public(), nil) + var validAt *time.Time - mockVerifier := NewMockVerifier(ctx.ctrl) - mockVerifier.EXPECT().Verify(vp.VerifiableCredential[0], true, true, validAt) + ctx := newMockContext(t) + ctx.keyResolver.EXPECT().ResolveKeyByID(vpSignerKeyID.String(), validAt, resolver.NutsSigningKeyType).Return(vdr.TestMethodDIDAPrivateKey().Public(), nil) - vcs, err := ctx.verifier.doVerifyVP(mockVerifier, vp, true, true, validAt) + mockVerifier := NewMockVerifier(ctx.ctrl) + mockVerifier.EXPECT().Verify(vp.VerifiableCredential[0], true, true, validAt) - assert.NoError(t, err) - assert.Len(t, vcs, 1) - }) - t.Run("error - VP verification fails (not valid at time)", func(t *testing.T) { - _ = json.Unmarshal([]byte(rawVP), &vp) + vcs, err := ctx.verifier.doVerifyVP(mockVerifier, vp, true, true, validAt) - var validAt time.Time + assert.NoError(t, err) + assert.Len(t, vcs, 1) + }) + t.Run("error - VP verification fails (not valid at time)", func(t *testing.T) { + _ = json.Unmarshal([]byte(rawVP), &vp) - ctx := newMockContext(t) + var validAt time.Time - mockVerifier := NewMockVerifier(ctx.ctrl) + ctx := newMockContext(t) - vcs, err := ctx.verifier.doVerifyVP(mockVerifier, vp, true, true, &validAt) + mockVerifier := NewMockVerifier(ctx.ctrl) - assert.EqualError(t, err, "verification error: presentation not valid at given time") - assert.Empty(t, vcs) - }) - t.Run("error - VC verification fails", func(t *testing.T) { - _ = json.Unmarshal([]byte(rawVP), &vp) + vcs, err := ctx.verifier.doVerifyVP(mockVerifier, vp, true, true, &validAt) - var validAt *time.Time + assert.EqualError(t, err, "verification error: presentation not valid at given time") + assert.Empty(t, vcs) + }) + t.Run("error - VC verification fails", func(t *testing.T) { + _ = json.Unmarshal([]byte(rawVP), &vp) - ctx := newMockContext(t) - ctx.keyResolver.EXPECT().ResolveKeyByID(vpSignerKeyID.String(), validAt, resolver.NutsSigningKeyType).Return(vdr.TestMethodDIDAPrivateKey().Public(), nil) + var validAt *time.Time - mockVerifier := NewMockVerifier(ctx.ctrl) - mockVerifier.EXPECT().Verify(vp.VerifiableCredential[0], false, true, validAt).Return(errors.New("invalid")) + ctx := newMockContext(t) + ctx.keyResolver.EXPECT().ResolveKeyByID(vpSignerKeyID.String(), validAt, resolver.NutsSigningKeyType).Return(vdr.TestMethodDIDAPrivateKey().Public(), nil) - vcs, err := ctx.verifier.doVerifyVP(mockVerifier, vp, true, false, validAt) + mockVerifier := NewMockVerifier(ctx.ctrl) + mockVerifier.EXPECT().Verify(vp.VerifiableCredential[0], false, true, validAt).Return(errors.New("invalid")) - assert.Error(t, err) - assert.Empty(t, vcs) - }) - t.Run("error - invalid signature", func(t *testing.T) { - _ = json.Unmarshal([]byte(rawVP), &vp) + vcs, err := ctx.verifier.doVerifyVP(mockVerifier, vp, true, false, validAt) - var validAt *time.Time + assert.Error(t, err) + assert.Empty(t, vcs) + }) + t.Run("error - invalid signature", func(t *testing.T) { + _ = json.Unmarshal([]byte(rawVP), &vp) - ctx := newMockContext(t) - // Return incorrect key, causing signature verification failure - ctx.keyResolver.EXPECT().ResolveKeyByID(vpSignerKeyID.String(), validAt, resolver.NutsSigningKeyType).Return(vdr.TestMethodDIDBPrivateKey().Public(), nil) + var validAt *time.Time - vcs, err := ctx.verifier.VerifyVP(vp, false, false, validAt) + ctx := newMockContext(t) + // Return incorrect key, causing signature verification failure + ctx.keyResolver.EXPECT().ResolveKeyByID(vpSignerKeyID.String(), validAt, resolver.NutsSigningKeyType).Return(vdr.TestMethodDIDBPrivateKey().Public(), nil) - assert.EqualError(t, err, "verification error: invalid signature: invalid proof signature: failed to verify signature using ecdsa") - assert.Empty(t, vcs) - }) - t.Run("error - signing key unknown", func(t *testing.T) { - _ = json.Unmarshal([]byte(rawVP), &vp) + vcs, err := ctx.verifier.VerifyVP(vp, false, false, validAt) - var validAt *time.Time + assert.EqualError(t, err, "verification error: invalid signature: invalid proof signature: failed to verify signature using ecdsa") + assert.Empty(t, vcs) + }) + t.Run("error - signing key unknown", func(t *testing.T) { + _ = json.Unmarshal([]byte(rawVP), &vp) - ctx := newMockContext(t) - // Return incorrect key, causing signature verification failure - ctx.keyResolver.EXPECT().ResolveKeyByID(vpSignerKeyID.String(), validAt, resolver.NutsSigningKeyType).Return(nil, resolver.ErrKeyNotFound) + var validAt *time.Time - vcs, err := ctx.verifier.VerifyVP(vp, false, false, validAt) + ctx := newMockContext(t) + // Return incorrect key, causing signature verification failure + ctx.keyResolver.EXPECT().ResolveKeyByID(vpSignerKeyID.String(), validAt, resolver.NutsSigningKeyType).Return(nil, resolver.ErrKeyNotFound) - assert.ErrorIs(t, err, resolver.ErrKeyNotFound) - assert.Empty(t, vcs) - }) - t.Run("error - invalid proof", func(t *testing.T) { - _ = json.Unmarshal([]byte(rawVP), &vp) + vcs, err := ctx.verifier.VerifyVP(vp, false, false, validAt) - vp.Proof = []interface{}{"invalid"} + assert.ErrorIs(t, err, resolver.ErrKeyNotFound) + assert.Empty(t, vcs) + }) + t.Run("error - invalid proof", func(t *testing.T) { + _ = json.Unmarshal([]byte(rawVP), &vp) - var validAt *time.Time + vp.Proof = []interface{}{"invalid"} - ctx := newMockContext(t) + var validAt *time.Time - vcs, err := ctx.verifier.VerifyVP(vp, false, false, validAt) + ctx := newMockContext(t) - assert.EqualError(t, err, "verification error: unsupported proof type: json: cannot unmarshal string into Go value of type proof.LDProof") - assert.Empty(t, vcs) - }) - t.Run("error - no proof", func(t *testing.T) { - _ = json.Unmarshal([]byte(rawVP), &vp) + vcs, err := ctx.verifier.VerifyVP(vp, false, false, validAt) - vp.Proof = nil + assert.EqualError(t, err, "verification error: unsupported proof type: json: cannot unmarshal string into Go value of type proof.LDProof") + assert.Empty(t, vcs) + }) + t.Run("error - no proof", func(t *testing.T) { + _ = json.Unmarshal([]byte(rawVP), &vp) - var validAt *time.Time + vp.Proof = nil - ctx := newMockContext(t) + var validAt *time.Time - vcs, err := ctx.verifier.VerifyVP(vp, false, false, validAt) + ctx := newMockContext(t) + + vcs, err := ctx.verifier.VerifyVP(vp, false, false, validAt) - assert.EqualError(t, err, "verification error: exactly 1 proof is expected") - assert.Empty(t, vcs) + assert.EqualError(t, err, "verification error: exactly 1 proof is expected") + assert.Empty(t, vcs) + }) }) } From bbc4aa11d7eccb1418353ca1c3918e1040c7e052 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Oct 2023 12:35:04 +0200 Subject: [PATCH 16/23] Bump google.golang.org/grpc from 1.58.3 to 1.59.0 (#2545) Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.58.3 to 1.59.0. - [Release notes](https://github.com/grpc/grpc-go/releases) - [Commits](https://github.com/grpc/grpc-go/compare/v1.58.3...v1.59.0) --- updated-dependencies: - dependency-name: google.golang.org/grpc dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index e24ca76502..63c12f7743 100644 --- a/go.mod +++ b/go.mod @@ -44,7 +44,7 @@ require ( go.uber.org/mock v0.3.0 golang.org/x/crypto v0.14.0 golang.org/x/time v0.3.0 - google.golang.org/grpc v1.58.3 + google.golang.org/grpc v1.59.0 google.golang.org/protobuf v1.31.0 gopkg.in/Regis24GmbH/go-phonetics.v2 v2.0.3 gopkg.in/yaml.v3 v3.0.1 @@ -161,7 +161,7 @@ require ( golang.org/x/sys v0.13.0 // indirect golang.org/x/term v0.13.0 // indirect golang.org/x/text v0.13.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect gopkg.in/Regis24GmbH/go-diacritics.v2 v2.0.3 // indirect rsc.io/qr v0.2.0 // indirect ) diff --git a/go.sum b/go.sum index 921017a8c7..d10b01a48e 100644 --- a/go.sum +++ b/go.sum @@ -189,8 +189,8 @@ github.com/golang-jwt/jwt/v4 v4.4.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE= -github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ= +github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo= +github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= @@ -796,8 +796,8 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98 google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 h1:bVf09lpb+OJbByTj913DRJioFFAjf/ZGxEz7MajTp2U= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= @@ -806,8 +806,8 @@ google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQ google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ= -google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= +google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= +google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= From f862be931b5adb621199e0f69a94fe16fd9874fa Mon Sep 17 00:00:00 2001 From: reinkrul Date: Mon, 23 Oct 2023 18:56:51 +0200 Subject: [PATCH 17/23] Switch to mrtron/base58 lib, maintainer looks more reputable (#2555) --- crypto/jwx.go | 4 ++-- crypto/jwx_test.go | 4 ++-- didman/didman.go | 4 ++-- go.mod | 4 ++-- vdr/didkey/resolver.go | 4 ++-- vdr/didkey/resolver_test.go | 8 ++++---- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/crypto/jwx.go b/crypto/jwx.go index 0d9c0655e4..29c022c605 100644 --- a/crypto/jwx.go +++ b/crypto/jwx.go @@ -32,10 +32,10 @@ import ( "github.com/lestrrat-go/jwx/jwk" "github.com/lestrrat-go/jwx/jws" "github.com/lestrrat-go/jwx/jwt" + "github.com/mr-tron/base58" "github.com/nuts-foundation/nuts-node/audit" "github.com/nuts-foundation/nuts-node/crypto/log" "github.com/nuts-foundation/nuts-node/crypto/storage/spi" - "github.com/shengdoushi/base58" ) // ErrUnsupportedSigningKey is returned when an unsupported private key is used to sign. Currently only ecdsa and rsa keys are supported @@ -449,5 +449,5 @@ func Thumbprint(key jwk.Key) (string, error) { if err != nil { return "", err } - return base58.Encode(pkHash[:], base58.BitcoinAlphabet), nil + return base58.EncodeAlphabet(pkHash[:], base58.BTCAlphabet), nil } diff --git a/crypto/jwx_test.go b/crypto/jwx_test.go index da6d7d8128..4d52f532be 100644 --- a/crypto/jwx_test.go +++ b/crypto/jwx_test.go @@ -36,7 +36,7 @@ import ( "time" "github.com/lestrrat-go/jwx/jwt" - "github.com/shengdoushi/base58" + "github.com/mr-tron/base58" "github.com/lestrrat-go/jwx/jwa" "github.com/lestrrat-go/jwx/jwk" @@ -542,7 +542,7 @@ func TestThumbprint(t *testing.T) { t.Run("rsa", func(t *testing.T) { // example from https://tools.ietf.org/html/rfc7638#page-3 testRsa := "{\"e\":\"AQAB\",\"kty\":\"RSA\",\"n\":\"0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw\"}" - expectedThumbPrint := base58.Encode([]byte{55, 54, 203, 177, 120, 124, 184, 48, 156, 119, 238, 140, 55, 5, 197, 225, 111, 251, 158, 133, 151, 21, 144, 31, 30, 76, 89, 177, 17, 130, 245, 123}, base58.BitcoinAlphabet) + expectedThumbPrint := base58.EncodeAlphabet([]byte{55, 54, 203, 177, 120, 124, 184, 48, 156, 119, 238, 140, 55, 5, 197, 225, 111, 251, 158, 133, 151, 21, 144, 31, 30, 76, 89, 177, 17, 130, 245, 123}, base58.BTCAlphabet) set, err := jwk.ParseString(testRsa) require.NoError(t, err) diff --git a/didman/didman.go b/didman/didman.go index 11822bd95b..0ff4584f0b 100644 --- a/didman/didman.go +++ b/didman/didman.go @@ -30,6 +30,7 @@ import ( "net/url" "sync" + "github.com/mr-tron/base58" ssi "github.com/nuts-foundation/go-did" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/go-did/vc" @@ -38,7 +39,6 @@ import ( "github.com/nuts-foundation/nuts-node/jsonld" "github.com/nuts-foundation/nuts-node/vcr" "github.com/nuts-foundation/nuts-node/vcr/credential" - "github.com/shengdoushi/base58" ) // ModuleName contains the name of this module: Didman @@ -601,7 +601,7 @@ func generateIDForService(id did.DID, service did.Service) ssi.URI { bytes, _ := json.Marshal(service) shaBytes := sha256.Sum256(bytes) d := id.URI() - d.Fragment = base58.Encode(shaBytes[:], base58.BitcoinAlphabet) + d.Fragment = base58.EncodeAlphabet(shaBytes[:], base58.BTCAlphabet) return d } diff --git a/go.mod b/go.mod index 63c12f7743..183ba12744 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/lestrrat-go/jwx v1.2.26 github.com/magiconair/properties v1.8.7 github.com/mdp/qrterminal/v3 v3.1.1 + github.com/mr-tron/base58 v1.1.3 github.com/multiformats/go-multicodec v0.9.0 github.com/nats-io/nats-server/v2 v2.10.3 github.com/nats-io/nats.go v1.31.0 @@ -32,7 +33,6 @@ require ( github.com/prometheus/client_golang v1.17.0 github.com/prometheus/client_model v0.5.0 github.com/redis/go-redis/v9 v9.2.1 - github.com/shengdoushi/base58 v1.0.0 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 @@ -124,7 +124,6 @@ require ( github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/mr-tron/base58 v1.1.3 // indirect github.com/multiformats/go-base32 v0.0.3 // indirect github.com/multiformats/go-base36 v0.1.0 // indirect github.com/multiformats/go-multibase v0.2.0 // indirect @@ -142,6 +141,7 @@ require ( github.com/robfig/cron/v3 v3.0.1 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect + github.com/shengdoushi/base58 v1.0.0 // indirect github.com/shopspring/decimal v1.3.1 // indirect github.com/sietseringers/go-sse v0.0.0-20200801161811-e2cf2c63ca50 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect diff --git a/vdr/didkey/resolver.go b/vdr/didkey/resolver.go index 2b3fc40478..6aeeaf4904 100644 --- a/vdr/didkey/resolver.go +++ b/vdr/didkey/resolver.go @@ -29,11 +29,11 @@ import ( "errors" "fmt" "github.com/lestrrat-go/jwx/x25519" + "github.com/mr-tron/base58" "github.com/multiformats/go-multicodec" ssi "github.com/nuts-foundation/go-did" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/nuts-node/vdr/resolver" - "github.com/shengdoushi/base58" "io" ) @@ -60,7 +60,7 @@ func (r Resolver) Resolve(id did.DID, _ *resolver.ResolveMetadata) (*did.Documen if len(encodedKey) == 0 || encodedKey[0] != 'z' { return nil, nil, errors.New("did:key does not start with 'z'") } - mcBytes, err := base58.Decode(encodedKey[1:], base58.BitcoinAlphabet) + mcBytes, err := base58.DecodeAlphabet(encodedKey[1:], base58.BTCAlphabet) if err != nil { return nil, nil, fmt.Errorf("did:key: invalid base58btc: %w", err) } diff --git a/vdr/didkey/resolver_test.go b/vdr/didkey/resolver_test.go index 8168569631..e6d1038568 100644 --- a/vdr/didkey/resolver_test.go +++ b/vdr/didkey/resolver_test.go @@ -27,9 +27,9 @@ import ( "crypto/x509" "encoding/binary" "encoding/json" + "github.com/mr-tron/base58" "github.com/multiformats/go-multicodec" "github.com/nuts-foundation/go-did/did" - "github.com/shengdoushi/base58" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "testing" @@ -198,11 +198,11 @@ func TestResolver_Resolve(t *testing.T) { }) t.Run("did:key ID is not valid base58btc encoded 'z'", func(t *testing.T) { _, _, err := Resolver{}.Resolve(did.MustParseDID("did:key:z291830129"), nil) - require.EqualError(t, err, "did:key: invalid base58btc: invalid base58 string") + require.EqualError(t, err, "did:key: invalid base58btc: invalid base58 digit ('0')") }) t.Run("invalid multicodec key type", func(t *testing.T) { _, _, err := Resolver{}.Resolve(did.MustParseDID("did:key:z"), nil) - require.EqualError(t, err, "did:key: invalid multicodec value: EOF") + require.EqualError(t, err, "did:key: invalid base58btc: zero length string") }) t.Run("unsupported key type", func(t *testing.T) { didKey := createDIDKey(multicodec.Aes256, []byte{1, 2, 3}) @@ -265,7 +265,7 @@ func TestNewResolver(t *testing.T) { func createDIDKey(keyType multicodec.Code, data []byte) string { mcBytes := append(binary.AppendUvarint([]byte{}, uint64(keyType)), data...) - return "did:key:z" + string(base58.Encode(mcBytes, base58.BitcoinAlphabet)) + return "did:key:z" + string(base58.EncodeAlphabet(mcBytes, base58.BTCAlphabet)) } func TestRoundTrip(t *testing.T) { From 08d3b813dc82cf2d343b6ed77e4ef2a9032775c2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Oct 2023 16:30:48 +0200 Subject: [PATCH 18/23] Bump github.com/mr-tron/base58 from 1.1.3 to 1.2.0 (#2557) Bumps [github.com/mr-tron/base58](https://github.com/mr-tron/base58) from 1.1.3 to 1.2.0. - [Release notes](https://github.com/mr-tron/base58/releases) - [Commits](https://github.com/mr-tron/base58/compare/v1.1.3...v1.2.0) --- updated-dependencies: - dependency-name: github.com/mr-tron/base58 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 183ba12744..d39fb8bfba 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/lestrrat-go/jwx v1.2.26 github.com/magiconair/properties v1.8.7 github.com/mdp/qrterminal/v3 v3.1.1 - github.com/mr-tron/base58 v1.1.3 + github.com/mr-tron/base58 v1.2.0 github.com/multiformats/go-multicodec v0.9.0 github.com/nats-io/nats-server/v2 v2.10.3 github.com/nats-io/nats.go v1.31.0 diff --git a/go.sum b/go.sum index d10b01a48e..37843740c4 100644 --- a/go.sum +++ b/go.sum @@ -415,8 +415,9 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/mr-tron/base58 v1.1.3 h1:v+sk57XuaCKGXpWtVBX8YJzO7hMGx4Aajh4TQbdEFdc= 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/multiformats/go-base32 v0.0.3 h1:tw5+NhuwaOjJCC5Pp82QuXbrmLzWg7uxlMFp8Nq/kkI= github.com/multiformats/go-base32 v0.0.3/go.mod h1:pLiuGC8y0QR3Ue4Zug5UzK9LjgbkL8NSQj0zQ5Nz/AA= github.com/multiformats/go-base36 v0.1.0 h1:JR6TyF7JjGd3m6FbLU2cOxhC0Li8z8dLNGQ89tUg4F4= From 5521f1720fc52fe7235baf4101ad8f54495034ea Mon Sep 17 00:00:00 2001 From: reinkrul Date: Wed, 25 Oct 2023 06:57:53 +0200 Subject: [PATCH 19/23] Do not load remote JSONLD contexts during unit tests (#2533) --- .../services/selfsigned/test/generate_test.go | 2 +- jsonld/test.go | 5 +- storage/leia_test.go | 2 +- .../test_assets/contexts/examples.ldjson | 53 +++++ vcr/assets/test_assets/contexts/odrl.ldjson | 200 ++++++++++++++++++ vcr/signature/json_web_signature_test.go | 3 +- vcr/signature/proof/jsonld_test.go | 5 +- 7 files changed, 262 insertions(+), 8 deletions(-) create mode 100644 vcr/assets/test_assets/contexts/examples.ldjson create mode 100644 vcr/assets/test_assets/contexts/odrl.ldjson diff --git a/auth/services/selfsigned/test/generate_test.go b/auth/services/selfsigned/test/generate_test.go index fc8b1c4372..298c526862 100644 --- a/auth/services/selfsigned/test/generate_test.go +++ b/auth/services/selfsigned/test/generate_test.go @@ -36,7 +36,7 @@ import ( func Test_GenerateTestData(t *testing.T) { store := false - contextLoader, _ := jsonld.NewContextLoader(false, jsonld.DefaultContextConfig()) + contextLoader := jsonld.NewTestJSONLDManager(t).DocumentLoader() createdTime := time.Date(2023, 4, 20, 9, 53, 3, 0, time.UTC) expirationTime := createdTime.Add(4 * 24 * time.Hour) diff --git a/jsonld/test.go b/jsonld/test.go index 9cfd9384f2..996d40e95b 100644 --- a/jsonld/test.go +++ b/jsonld/test.go @@ -154,7 +154,10 @@ func NewTestJSONLDManager(t *testing.T) JSONLD { t.Helper() contextConfig := DefaultContextConfig() + contextConfig.RemoteAllowList = nil contextConfig.LocalFileMapping["http://example.org/credentials/V1"] = "test_assets/contexts/test.ldjson" + contextConfig.LocalFileMapping["https://www.w3.org/2018/credentials/examples/v1"] = "test_assets/contexts/examples.ldjson" + contextConfig.LocalFileMapping["https://www.w3.org/ns/odrl.jsonld"] = "test_assets/contexts/odrl.ldjson" loader := NewMappedDocumentLoader(contextConfig.LocalFileMapping, NewEmbeddedFSDocumentLoader(assets.Assets, @@ -162,7 +165,7 @@ func NewTestJSONLDManager(t *testing.T) JSONLD { NewEmbeddedFSDocumentLoader(assets.TestAssets, // Last in the chain is the defaultLoader which can resolve // local files and remote (via http) context documents - ld.NewDefaultDocumentLoader(nil)))) + nil))) manager := testContextManager{loader: loader} diff --git a/storage/leia_test.go b/storage/leia_test.go index e331ee4486..c79b308758 100644 --- a/storage/leia_test.go +++ b/storage/leia_test.go @@ -182,7 +182,7 @@ func newStoreInDir(t *testing.T, testDir string, backupConfig LeiaBackupConfigur backupStorePath := path.Join(testDir, "vcr", "backup-private-credentials.db") backupStore, err := bbolt.CreateBBoltStore(backupStorePath) require.NoError(t, err) - leiaStore, err := leia.NewStore(issuerStorePath) + leiaStore, err := leia.NewStore(issuerStorePath, leia.WithDocumentLoader(jsonld.NewTestJSONLDManager(t).DocumentLoader())) require.NoError(t, err) store, err := NewKVBackedLeiaStore(leiaStore, backupStore) require.NoError(t, err) diff --git a/vcr/assets/test_assets/contexts/examples.ldjson b/vcr/assets/test_assets/contexts/examples.ldjson new file mode 100644 index 0000000000..173626c762 --- /dev/null +++ b/vcr/assets/test_assets/contexts/examples.ldjson @@ -0,0 +1,53 @@ +{ + "@context": [{ + "@version": 1.1 + },"https://www.w3.org/ns/odrl.jsonld", { + "ex": "https://example.org/examples#", + "schema": "http://schema.org/", + "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + + "3rdPartyCorrelation": "ex:3rdPartyCorrelation", + "AllVerifiers": "ex:AllVerifiers", + "Archival": "ex:Archival", + "BachelorDegree": "ex:BachelorDegree", + "Child": "ex:Child", + "CLCredentialDefinition2019": "ex:CLCredentialDefinition2019", + "CLSignature2019": "ex:CLSignature2019", + "IssuerPolicy": "ex:IssuerPolicy", + "HolderPolicy": "ex:HolderPolicy", + "Mother": "ex:Mother", + "RelationshipCredential": "ex:RelationshipCredential", + "UniversityDegreeCredential": "ex:UniversityDegreeCredential", + "AlumniCredential": "ex:AlumniCredential", + "DisputeCredential": "ex:DisputeCredential", + "PrescriptionCredential": "ex:PrescriptionCredential", + "ZkpExampleSchema2018": "ex:ZkpExampleSchema2018", + + "issuerData": "ex:issuerData", + "attributes": "ex:attributes", + "signature": "ex:signature", + "signatureCorrectnessProof": "ex:signatureCorrectnessProof", + "primaryProof": "ex:primaryProof", + "nonRevocationProof": "ex:nonRevocationProof", + + "alumniOf": {"@id": "schema:alumniOf", "@type": "rdf:HTML"}, + "child": {"@id": "ex:child", "@type": "@id"}, + "degree": "ex:degree", + "degreeType": "ex:degreeType", + "degreeSchool": "ex:degreeSchool", + "college": "ex:college", + "name": {"@id": "schema:name", "@type": "rdf:HTML"}, + "givenName": "schema:givenName", + "familyName": "schema:familyName", + "parent": {"@id": "ex:parent", "@type": "@id"}, + "referenceId": "ex:referenceId", + "documentPresence": "ex:documentPresence", + "evidenceDocument": "ex:evidenceDocument", + "spouse": "schema:spouse", + "subjectPresence": "ex:subjectPresence", + "verifier": {"@id": "ex:verifier", "@type": "@id"}, + "currentStatus": "ex:currentStatus", + "statusReason": "ex:statusReason", + "prescription": "ex:prescription" + }] +} diff --git a/vcr/assets/test_assets/contexts/odrl.ldjson b/vcr/assets/test_assets/contexts/odrl.ldjson new file mode 100644 index 0000000000..e779e87f7e --- /dev/null +++ b/vcr/assets/test_assets/contexts/odrl.ldjson @@ -0,0 +1,200 @@ +{ + "@context": { + "odrl": "http://www.w3.org/ns/odrl/2/", + "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "rdfs": "http://www.w3.org/2000/01/rdf-schema#", + "owl": "http://www.w3.org/2002/07/owl#", + "skos": "http://www.w3.org/2004/02/skos/core#", + "dct": "http://purl.org/dc/terms/", + "xsd": "http://www.w3.org/2001/XMLSchema#", + "vcard": "http://www.w3.org/2006/vcard/ns#", + "foaf": "http://xmlns.com/foaf/0.1/", + "schema": "http://schema.org/", + "cc": "http://creativecommons.org/ns#", + + "uid": "@id", + "type": "@type", + + "Policy": "odrl:Policy", + "Rule": "odrl:Rule", + "profile": {"@type": "@id", "@id": "odrl:profile"}, + + "inheritFrom": {"@type": "@id", "@id": "odrl:inheritFrom"}, + + "ConflictTerm": "odrl:ConflictTerm", + "conflict": {"@type": "@vocab", "@id": "odrl:conflict"}, + "perm": "odrl:perm", + "prohibit": "odrl:prohibit", + "invalid": "odrl:invalid", + + "Agreement": "odrl:Agreement", + "Assertion": "odrl:Assertion", + "Offer": "odrl:Offer", + "Privacy": "odrl:Privacy", + "Request": "odrl:Request", + "Set": "odrl:Set", + "Ticket": "odrl:Ticket", + + "Asset": "odrl:Asset", + "AssetCollection": "odrl:AssetCollection", + "relation": {"@type": "@id", "@id": "odrl:relation"}, + "hasPolicy": {"@type": "@id", "@id": "odrl:hasPolicy"}, + + "target": {"@type": "@id", "@id": "odrl:target"}, + "output": {"@type": "@id", "@id": "odrl:output"}, + + "partOf": {"@type": "@id", "@id": "odrl:partOf"}, + "source": {"@type": "@id", "@id": "odrl:source"}, + + "Party": "odrl:Party", + "PartyCollection": "odrl:PartyCollection", + "function": {"@type": "@vocab", "@id": "odrl:function"}, + "PartyScope": "odrl:PartyScope", + + "assignee": {"@type": "@id", "@id": "odrl:assignee"}, + "assigner": {"@type": "@id", "@id": "odrl:assigner"}, + "assigneeOf": {"@type": "@id", "@id": "odrl:assigneeOf"}, + "assignerOf": {"@type": "@id", "@id": "odrl:assignerOf"}, + "attributedParty": {"@type": "@id", "@id": "odrl:attributedParty"}, + "attributingParty": {"@type": "@id", "@id": "odrl:attributingParty"}, + "compensatedParty": {"@type": "@id", "@id": "odrl:compensatedParty"}, + "compensatingParty": {"@type": "@id", "@id": "odrl:compensatingParty"}, + "consentingParty": {"@type": "@id", "@id": "odrl:consentingParty"}, + "consentedParty": {"@type": "@id", "@id": "odrl:consentedParty"}, + "informedParty": {"@type": "@id", "@id": "odrl:informedParty"}, + "informingParty": {"@type": "@id", "@id": "odrl:informingParty"}, + "trackingParty": {"@type": "@id", "@id": "odrl:trackingParty"}, + "trackedParty": {"@type": "@id", "@id": "odrl:trackedParty"}, + "contractingParty": {"@type": "@id", "@id": "odrl:contractingParty"}, + "contractedParty": {"@type": "@id", "@id": "odrl:contractedParty"}, + + "Action": "odrl:Action", + "action": {"@type": "@vocab", "@id": "odrl:action"}, + "includedIn": {"@type": "@id", "@id": "odrl:includedIn"}, + "implies": {"@type": "@id", "@id": "odrl:implies"}, + + "Permission": "odrl:Permission", + "permission": {"@type": "@id", "@id": "odrl:permission"}, + + "Prohibition": "odrl:Prohibition", + "prohibition": {"@type": "@id", "@id": "odrl:prohibition"}, + + "obligation": {"@type": "@id", "@id": "odrl:obligation"}, + + "use": "odrl:use", + "grantUse": "odrl:grantUse", + "aggregate": "odrl:aggregate", + "annotate": "odrl:annotate", + "anonymize": "odrl:anonymize", + "archive": "odrl:archive", + "concurrentUse": "odrl:concurrentUse", + "derive": "odrl:derive", + "digitize": "odrl:digitize", + "display": "odrl:display", + "distribute": "odrl:distribute", + "execute": "odrl:execute", + "extract": "odrl:extract", + "give": "odrl:give", + "index": "odrl:index", + "install": "odrl:install", + "modify": "odrl:modify", + "move": "odrl:move", + "play": "odrl:play", + "present": "odrl:present", + "print": "odrl:print", + "read": "odrl:read", + "reproduce": "odrl:reproduce", + "sell": "odrl:sell", + "stream": "odrl:stream", + "textToSpeech": "odrl:textToSpeech", + "transfer": "odrl:transfer", + "transform": "odrl:transform", + "translate": "odrl:translate", + + "Duty": "odrl:Duty", + "duty": {"@type": "@id", "@id": "odrl:duty"}, + "consequence": {"@type": "@id", "@id": "odrl:consequence"}, + "remedy": {"@type": "@id", "@id": "odrl:remedy"}, + + "acceptTracking": "odrl:acceptTracking", + "attribute": "odrl:attribute", + "compensate": "odrl:compensate", + "delete": "odrl:delete", + "ensureExclusivity": "odrl:ensureExclusivity", + "include": "odrl:include", + "inform": "odrl:inform", + "nextPolicy": "odrl:nextPolicy", + "obtainConsent": "odrl:obtainConsent", + "reviewPolicy": "odrl:reviewPolicy", + "uninstall": "odrl:uninstall", + "watermark": "odrl:watermark", + + "Constraint": "odrl:Constraint", + "LogicalConstraint": "odrl:LogicalConstraint", + "constraint": {"@type": "@id", "@id": "odrl:constraint"}, + "refinement": {"@type": "@id", "@id": "odrl:refinement"}, + "Operator": "odrl:Operator", + "operator": {"@type": "@vocab", "@id": "odrl:operator"}, + "RightOperand": "odrl:RightOperand", + "rightOperand": "odrl:rightOperand", + "rightOperandReference":{"@type": "xsd:anyURI", "@id": "odrl:rightOperandReference"}, + "LeftOperand": "odrl:LeftOperand", + "leftOperand": {"@type": "@vocab", "@id": "odrl:leftOperand"}, + "unit": "odrl:unit", + "dataType": {"@type": "xsd:anyType", "@id": "odrl:datatype"}, + "status": "odrl:status", + + "absolutePosition": "odrl:absolutePosition", + "absoluteSpatialPosition": "odrl:absoluteSpatialPosition", + "absoluteTemporalPosition":"odrl:absoluteTemporalPosition", + "absoluteSize": "odrl:absoluteSize", + "count": "odrl:count", + "dateTime": "odrl:dateTime", + "delayPeriod": "odrl:delayPeriod", + "deliveryChannel": "odrl:deliveryChannel", + "elapsedTime": "odrl:elapsedTime", + "event": "odrl:event", + "fileFormat": "odrl:fileFormat", + "industry": "odrl:industry:", + "language": "odrl:language", + "media": "odrl:media", + "meteredTime": "odrl:meteredTime", + "payAmount": "odrl:payAmount", + "percentage": "odrl:percentage", + "product": "odrl:product", + "purpose": "odrl:purpose", + "recipient": "odrl:recipient", + "relativePosition": "odrl:relativePosition", + "relativeSpatialPosition": "odrl:relativeSpatialPosition", + "relativeTemporalPosition":"odrl:relativeTemporalPosition", + "relativeSize": "odrl:relativeSize", + "resolution": "odrl:resolution", + "spatial": "odrl:spatial", + "spatialCoordinates": "odrl:spatialCoordinates", + "systemDevice": "odrl:systemDevice", + "timeInterval": "odrl:timeInterval", + "unitOfCount": "odrl:unitOfCount", + "version": "odrl:version", + "virtualLocation": "odrl:virtualLocation", + + "eq": "odrl:eq", + "gt": "odrl:gt", + "gteq": "odrl:gteq", + "lt": "odrl:lt", + "lteq": "odrl:lteq", + "neq": "odrl:neg", + "isA": "odrl:isA", + "hasPart": "odrl:hasPart", + "isPartOf": "odrl:isPartOf", + "isAllOf": "odrl:isAllOf", + "isAnyOf": "odrl:isAnyOf", + "isNoneOf": "odrl:isNoneOf", + "or": "odrl:or", + "xone": "odrl:xone", + "and": "odrl:and", + "andSequence": "odrl:andSequence", + + "policyUsage": "odrl:policyUsage" + + } +} diff --git a/vcr/signature/json_web_signature_test.go b/vcr/signature/json_web_signature_test.go index d54fee5d17..c39d3c0e68 100644 --- a/vcr/signature/json_web_signature_test.go +++ b/vcr/signature/json_web_signature_test.go @@ -57,8 +57,7 @@ func TestJsonWebSignature2020_CanonicalizeDocument(t *testing.T) { }) t.Run("simple document with resolvable context", func(t *testing.T) { - contextLoader, err := jsonld.NewContextLoader(false, jsonld.DefaultContextConfig()) - assert.NoError(t, err) + contextLoader := jsonld.NewTestJSONLDManager(t).DocumentLoader() sig := JSONWebSignature2020{ContextLoader: contextLoader} doc := map[string]interface{}{ diff --git a/vcr/signature/proof/jsonld_test.go b/vcr/signature/proof/jsonld_test.go index 365169c445..a1a3a07c78 100644 --- a/vcr/signature/proof/jsonld_test.go +++ b/vcr/signature/proof/jsonld_test.go @@ -81,8 +81,7 @@ func TestLDProof_Verify(t *testing.T) { signedDocument := SignedDocument{} require.NoError(t, json.Unmarshal([]byte(vc_0), &signedDocument)) - contextLoader, err := jsonld.NewContextLoader(true, jsonld.ContextsConfig{}) - require.NoError(t, err) + contextLoader := jsonld.NewTestJSONLDManager(t).DocumentLoader() t.Run("ok - JSONWebSignature2020 test vector", func(t *testing.T) { ldProof := LDProof{} @@ -169,7 +168,7 @@ func TestLDProof_Sign(t *testing.T) { kid := "did:nuts:123#abc" testKey := crypto.NewTestKey(kid) - contextLoader, _ := jsonld.NewContextLoader(false, jsonld.DefaultContextConfig()) + contextLoader := jsonld.NewTestJSONLDManager(t).DocumentLoader() t.Run("sign and verify a document", func(t *testing.T) { now := time.Now() From c5afa092eac9952ef38e94c0012f66573839481a Mon Sep 17 00:00:00 2001 From: reinkrul Date: Wed, 25 Oct 2023 11:28:52 +0200 Subject: [PATCH 20/23] IAM: createAccessToken() func for handling s2s access token requests and token introspection (#2558) --- auth/api/iam/api_test.go | 8 +++-- auth/api/iam/s2s_vptoken.go | 53 ++++++++++++++++++++++++++++++++ auth/api/iam/s2s_vptoken_test.go | 37 ++++++++++++++++++++-- storage/test.go | 2 +- 4 files changed, 94 insertions(+), 6 deletions(-) diff --git a/auth/api/iam/api_test.go b/auth/api/iam/api_test.go index bff9a8c8c2..fd56e2a2c8 100644 --- a/auth/api/iam/api_test.go +++ b/auth/api/iam/api_test.go @@ -27,6 +27,7 @@ import ( "github.com/nuts-foundation/nuts-node/audit" "github.com/nuts-foundation/nuts-node/auth" "github.com/nuts-foundation/nuts-node/core" + "github.com/nuts-foundation/nuts-node/storage" "github.com/nuts-foundation/nuts-node/vdr" "github.com/nuts-foundation/nuts-node/vdr/resolver" "github.com/stretchr/testify/assert" @@ -241,18 +242,21 @@ func newTestClient(t testing.TB) *testCtx { publicURL, err := url.Parse("https://example.com") require.NoError(t, err) ctrl := gomock.NewController(t) + storageEngine := storage.NewTestStorageEngine(t) authnServices := auth.NewMockAuthenticationServices(ctrl) authnServices.EXPECT().PublicURL().Return(publicURL).AnyTimes() resolver := resolver.NewMockDIDResolver(ctrl) vdr := vdr.NewMockVDR(ctrl) vdr.EXPECT().Resolver().Return(resolver).AnyTimes() + return &testCtx{ authnServices: authnServices, resolver: resolver, vdr: vdr, client: &Wrapper{ - auth: authnServices, - vdr: vdr, + auth: authnServices, + vdr: vdr, + storageEngine: storageEngine, }, } } diff --git a/auth/api/iam/s2s_vptoken.go b/auth/api/iam/s2s_vptoken.go index 65fa221404..7c16066277 100644 --- a/auth/api/iam/s2s_vptoken.go +++ b/auth/api/iam/s2s_vptoken.go @@ -20,14 +20,27 @@ package iam import ( "context" + "crypto/rand" + "encoding/base64" "errors" + "fmt" "github.com/labstack/echo/v4" "github.com/nuts-foundation/go-did/did" + "github.com/nuts-foundation/go-did/vc" "github.com/nuts-foundation/nuts-node/core" + "github.com/nuts-foundation/nuts-node/storage" "github.com/nuts-foundation/nuts-node/vdr/resolver" "net/http" + "time" ) +// secretSizeBits is the size of the generated random secrets (access tokens, pre-authorized codes) in bits. +const secretSizeBits = 128 + +// accessTokenValidity defines how long access tokens are valid. +// TODO: Might want to make this configurable at some point +const accessTokenValidity = 15 * time.Minute + // serviceToService adds support for service-to-service OAuth2 flows, // which uses a custom vp_token grant to authenticate calls to the token endpoint. // Clients first call the presentation definition endpoint to get a presentation definition for the desired scope, @@ -99,3 +112,43 @@ func (r Wrapper) RequestAccessToken(ctx context.Context, request RequestAccessTo return RequestAccessToken200JSONResponse{}, nil } + +func (r Wrapper) createAccessToken(issuer did.DID, issueTime time.Time, presentation vc.VerifiablePresentation, scope string) (*TokenResponse, error) { + accessToken := AccessToken{ + Token: generateCode(), + Issuer: issuer.String(), + Expiration: issueTime.Add(accessTokenValidity), + Presentation: presentation, + } + err := r.accessTokenStore(issuer).Put(accessToken.Token, accessToken) + if err != nil { + return nil, fmt.Errorf("unable to store access token: %w", err) + } + expiresIn := int(accessTokenValidity.Seconds()) + return &TokenResponse{ + AccessToken: accessToken.Token, + ExpiresIn: &expiresIn, + Scope: &scope, + TokenType: "bearer", + }, nil +} + +func (r Wrapper) accessTokenStore(issuer did.DID) storage.SessionStore { + return r.storageEngine.GetSessionDatabase().GetStore(accessTokenValidity, "s2s", issuer.String(), "accesstoken") +} + +func generateCode() string { + buf := make([]byte, secretSizeBits/8) + _, err := rand.Read(buf) + if err != nil { + panic(err) + } + return base64.URLEncoding.EncodeToString(buf) +} + +type AccessToken struct { + Token string + Issuer string + Expiration time.Time + Presentation vc.VerifiablePresentation +} diff --git a/auth/api/iam/s2s_vptoken_test.go b/auth/api/iam/s2s_vptoken_test.go index e438585179..858aab47e4 100644 --- a/auth/api/iam/s2s_vptoken_test.go +++ b/auth/api/iam/s2s_vptoken_test.go @@ -19,12 +19,14 @@ package iam import ( - "github.com/nuts-foundation/nuts-node/vdr/resolver" - "testing" - "github.com/nuts-foundation/go-did/did" + "github.com/nuts-foundation/go-did/vc" + "github.com/nuts-foundation/nuts-node/jsonld" + "github.com/nuts-foundation/nuts-node/vdr/resolver" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "testing" + "time" ) func TestWrapper_RequestAccessToken(t *testing.T) { @@ -86,3 +88,32 @@ func TestWrapper_RequestAccessToken(t *testing.T) { assert.EqualError(t, err, "verifier not found: unable to find the DID document") }) } + +func TestWrapper_createAccessToken(t *testing.T) { + credential, err := vc.ParseVerifiableCredential(jsonld.TestOrganizationCredential) + require.NoError(t, err) + presentation := vc.VerifiablePresentation{ + VerifiableCredential: []vc.VerifiableCredential{*credential}, + } + t.Run("ok", func(t *testing.T) { + ctx := newTestClient(t) + + accessToken, err := ctx.client.createAccessToken(issuerDID, time.Now(), presentation, "everything") + + require.NoError(t, err) + assert.NotEmpty(t, accessToken.AccessToken) + assert.Equal(t, "bearer", accessToken.TokenType) + assert.Equal(t, 900, *accessToken.ExpiresIn) + assert.Equal(t, "everything", *accessToken.Scope) + + var storedToken AccessToken + err = ctx.client.accessTokenStore(issuerDID).Get(accessToken.AccessToken, &storedToken) + require.NoError(t, err) + assert.Equal(t, accessToken.AccessToken, storedToken.Token) + expectedVPJSON, _ := presentation.MarshalJSON() + actualVPJSON, _ := storedToken.Presentation.MarshalJSON() + assert.JSONEq(t, string(expectedVPJSON), string(actualVPJSON)) + assert.Equal(t, issuerDID.String(), storedToken.Issuer) + assert.NotEmpty(t, storedToken.Expiration) + }) +} diff --git a/storage/test.go b/storage/test.go index d1c6c07116..95fb052a18 100644 --- a/storage/test.go +++ b/storage/test.go @@ -34,7 +34,7 @@ func NewTestStorageEngineInDir(dir string) Engine { return result } -func NewTestStorageEngine(t *testing.T) Engine { +func NewTestStorageEngine(t testing.TB) Engine { oldOpts := append(DefaultBBoltOptions[:]) t.Cleanup(func() { DefaultBBoltOptions = oldOpts From c8abcf3201ff050688154aae515a6bff6e1ea443 Mon Sep 17 00:00:00 2001 From: reinkrul Date: Wed, 25 Oct 2023 14:59:48 +0200 Subject: [PATCH 21/23] PEX: Validate Presentation Definitions and Submissions using JSON schema (#2556) --- auth/api/iam/openid4vp_test.go | 2 +- .../test/presentation_definition_mapping.json | 7 +- go.mod | 2 + go.sum | 2 + vcr/pe/schema/README.md | 14 +- vcr/pe/schema/gen/README.md | 11 + vcr/pe/schema/{ => gen}/go.mod | 2 +- vcr/pe/schema/{ => gen}/go.sum | 0 vcr/pe/schema/{ => gen}/main.go | 2 +- vcr/pe/schema/v2/input-descriptor.json | 220 +++++++++++ vcr/pe/schema/v2/json-schema-draft-07.json | 172 +++++++++ ...-definition-claim-format-designations.json | 30 ++ .../v2/presentation-definition-envelope.json | 353 ++++++++++++++++++ vcr/pe/schema/v2/presentation-definition.json | 345 +++++++++++++++++ ...-submission-claim-format-designations.json | 11 + vcr/pe/schema/v2/presentation-submission.json | 40 ++ vcr/pe/schema/v2/schema.go | 104 ++++++ vcr/pe/schema/v2/schema_test.go | 40 ++ vcr/pe/schema/v2/submission-requirement.json | 50 +++ vcr/pe/schema/v2/submission-requirements.json | 102 +++++ vcr/pe/store.go | 26 +- vcr/pe/store_test.go | 8 + vcr/pe/submission.go | 56 +++ vcr/pe/submission_test.go | 37 ++ vcr/pe/test/definition_mapping.json | 7 +- vcr/pe/test/invalid_definition_mapping.json | 5 + vcr/pe/types.go | 17 - 27 files changed, 1627 insertions(+), 38 deletions(-) create mode 100644 vcr/pe/schema/gen/README.md rename vcr/pe/schema/{ => gen}/go.mod (60%) rename vcr/pe/schema/{ => gen}/go.sum (100%) rename vcr/pe/schema/{ => gen}/main.go (96%) create mode 100644 vcr/pe/schema/v2/input-descriptor.json create mode 100644 vcr/pe/schema/v2/json-schema-draft-07.json create mode 100644 vcr/pe/schema/v2/presentation-definition-claim-format-designations.json create mode 100644 vcr/pe/schema/v2/presentation-definition-envelope.json create mode 100644 vcr/pe/schema/v2/presentation-definition.json create mode 100644 vcr/pe/schema/v2/presentation-submission-claim-format-designations.json create mode 100644 vcr/pe/schema/v2/presentation-submission.json create mode 100644 vcr/pe/schema/v2/schema.go create mode 100644 vcr/pe/schema/v2/schema_test.go create mode 100644 vcr/pe/schema/v2/submission-requirement.json create mode 100644 vcr/pe/schema/v2/submission-requirements.json create mode 100644 vcr/pe/submission.go create mode 100644 vcr/pe/submission_test.go create mode 100644 vcr/pe/test/invalid_definition_mapping.json diff --git a/auth/api/iam/openid4vp_test.go b/auth/api/iam/openid4vp_test.go index 22920d4a92..9da7f5b192 100644 --- a/auth/api/iam/openid4vp_test.go +++ b/auth/api/iam/openid4vp_test.go @@ -93,7 +93,7 @@ func TestWrapper_handlePresentationRequest(t *testing.T) { t.Run("with scope", func(t *testing.T) { ctrl := gomock.NewController(t) peStore := &pe.DefinitionResolver{} - _ = peStore.LoadFromFile("test/presentation_definition_mapping.json") + require.NoError(t, peStore.LoadFromFile("test/presentation_definition_mapping.json")) mockVDR := vdr.NewMockVDR(ctrl) mockVCR := vcr.NewMockVCR(ctrl) mockWallet := holder.NewMockWallet(ctrl) diff --git a/auth/api/iam/test/presentation_definition_mapping.json b/auth/api/iam/test/presentation_definition_mapping.json index 4b64377e17..75e79eca18 100644 --- a/auth/api/iam/test/presentation_definition_mapping.json +++ b/auth/api/iam/test/presentation_definition_mapping.json @@ -1 +1,6 @@ -{"eOverdracht-overdrachtsbericht":{}} \ No newline at end of file +{ + "eOverdracht-overdrachtsbericht": { + "id": "eOverdracht", + "input_descriptors": [] + } +} \ No newline at end of file diff --git a/go.mod b/go.mod index d39fb8bfba..64d8932617 100644 --- a/go.mod +++ b/go.mod @@ -51,6 +51,8 @@ require ( schneider.vip/problem v1.8.1 ) +require github.com/santhosh-tekuri/jsonschema v1.2.4 + require ( github.com/PaesslerAG/gval v1.2.2 // indirect github.com/alexandrevicenzi/go-sse v1.6.0 // indirect diff --git a/go.sum b/go.sum index 37843740c4..72a174816e 100644 --- a/go.sum +++ b/go.sum @@ -531,6 +531,8 @@ github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= 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/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/shengdoushi/base58 v1.0.0 h1:tGe4o6TmdXFJWoI31VoSWvuaKxf0Px3gqa3sUWhAxBs= github.com/shengdoushi/base58 v1.0.0/go.mod h1:m5uIILfzcKMw6238iWAhP4l3s5+uXyF3+bJKUNhAL9I= diff --git a/vcr/pe/schema/README.md b/vcr/pe/schema/README.md index fac22a7041..bf6b681e8b 100644 --- a/vcr/pe/schema/README.md +++ b/vcr/pe/schema/README.md @@ -1,11 +1,3 @@ -# generate structs from JSON schema - -From this directory, run: - -```shell -go run . -``` - -It'll generate `generated.go` within the `pe` package. -The generated code is not really useful, but it could serve as a guide for the types that are expected by the API. -The output of `generated.go` is copied to `types.go` \ No newline at end of file +Schemas files were taken from: +- https://github.com/decentralized-identity/presentation-exchange/tree/main/schemas +- https://github.com/decentralized-identity/claim-format-registry/tree/main/schemas \ No newline at end of file diff --git a/vcr/pe/schema/gen/README.md b/vcr/pe/schema/gen/README.md new file mode 100644 index 0000000000..fac22a7041 --- /dev/null +++ b/vcr/pe/schema/gen/README.md @@ -0,0 +1,11 @@ +# generate structs from JSON schema + +From this directory, run: + +```shell +go run . +``` + +It'll generate `generated.go` within the `pe` package. +The generated code is not really useful, but it could serve as a guide for the types that are expected by the API. +The output of `generated.go` is copied to `types.go` \ No newline at end of file diff --git a/vcr/pe/schema/go.mod b/vcr/pe/schema/gen/go.mod similarity index 60% rename from vcr/pe/schema/go.mod rename to vcr/pe/schema/gen/go.mod index db1192449c..b872a8ae8e 100644 --- a/vcr/pe/schema/go.mod +++ b/vcr/pe/schema/gen/go.mod @@ -1,4 +1,4 @@ -module github.com/nuts-foundation/nuts-node/vcr/pe/schema +module github.com/nuts-foundation/nuts-node/vcr/pe/gen/schema go 1.21 diff --git a/vcr/pe/schema/go.sum b/vcr/pe/schema/gen/go.sum similarity index 100% rename from vcr/pe/schema/go.sum rename to vcr/pe/schema/gen/go.sum diff --git a/vcr/pe/schema/main.go b/vcr/pe/schema/gen/main.go similarity index 96% rename from vcr/pe/schema/main.go rename to vcr/pe/schema/gen/main.go index 2eb767ecd4..7b3e4f0faf 100644 --- a/vcr/pe/schema/main.go +++ b/vcr/pe/schema/gen/main.go @@ -44,7 +44,7 @@ func main() { os.Exit(1) } - f, err := os.OpenFile("../generated.go", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + f, err := os.OpenFile("generated.go", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) if err != nil { fmt.Fprintln(os.Stderr, "Error opening output file: ", err) diff --git a/vcr/pe/schema/v2/input-descriptor.json b/vcr/pe/schema/v2/input-descriptor.json new file mode 100644 index 0000000000..2112e900f8 --- /dev/null +++ b/vcr/pe/schema/v2/input-descriptor.json @@ -0,0 +1,220 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Input Descriptor", + "definitions": { + "status_directive": { + "type": "object", + "additionalProperties": false, + "properties": { + "directive": { + "type": "string", + "enum": [ + "required", + "allowed", + "disallowed" + ] + }, + "type": { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + } + }, + "field": { + "type": "object", + "oneOf": [ + { + "properties": { + "id": { + "type": "string" + }, + "optional": { + "type": "boolean" + }, + "path": { + "type": "array", + "items": { + "type": "string" + } + }, + "purpose": { + "type": "string" + }, + "intent_to_retain": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "filter": { + "$ref": "http://json-schema.org/draft-07/schema#" + } + }, + "required": [ + "path" + ], + "additionalProperties": false + }, + { + "properties": { + "id": { + "type": "string" + }, + "optional": { + "type": "boolean" + }, + "path": { + "type": "array", + "items": { + "type": "string" + } + }, + "purpose": { + "type": "string" + }, + "intent_to_retain": { + "type": "boolean" + }, + "filter": { + "$ref": "http://json-schema.org/draft-07/schema#" + }, + "name": { + "type": "string" + }, + "predicate": { + "type": "string", + "enum": [ + "required", + "preferred" + ] + } + }, + "required": [ + "path", + "filter", + "predicate" + ], + "additionalProperties": false + } + ] + } + }, + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "purpose": { + "type": "string" + }, + "group": { + "type": "array", + "items": { + "type": "string" + } + }, + "constraints": { + "type": "object", + "additionalProperties": false, + "properties": { + "limit_disclosure": { + "type": "string", + "enum": [ + "required", + "preferred" + ] + }, + "statuses": { + "type": "object", + "additionalProperties": false, + "properties": { + "active": { + "$ref": "#/definitions/status_directive" + }, + "suspended": { + "$ref": "#/definitions/status_directive" + }, + "revoked": { + "$ref": "#/definitions/status_directive" + } + } + }, + "fields": { + "type": "array", + "items": { + "$ref": "#/definitions/field" + } + }, + "subject_is_issuer": { + "type": "string", + "enum": [ + "required", + "preferred" + ] + }, + "is_holder": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "field_id": { + "type": "array", + "items": { + "type": "string" + } + }, + "directive": { + "type": "string", + "enum": [ + "required", + "preferred" + ] + } + }, + "required": [ + "field_id", + "directive" + ] + } + }, + "same_subject": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "field_id": { + "type": "array", + "items": { + "type": "string" + } + }, + "directive": { + "type": "string", + "enum": [ + "required", + "preferred" + ] + } + }, + "required": [ + "field_id", + "directive" + ] + } + } + } + } + }, + "required": [ + "id" + ] +} diff --git a/vcr/pe/schema/v2/json-schema-draft-07.json b/vcr/pe/schema/v2/json-schema-draft-07.json new file mode 100644 index 0000000000..fb92c7f756 --- /dev/null +++ b/vcr/pe/schema/v2/json-schema-draft-07.json @@ -0,0 +1,172 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://json-schema.org/draft-07/schema#", + "title": "Core schema meta-schema", + "definitions": { + "schemaArray": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#" } + }, + "nonNegativeInteger": { + "type": "integer", + "minimum": 0 + }, + "nonNegativeIntegerDefault0": { + "allOf": [ + { "$ref": "#/definitions/nonNegativeInteger" }, + { "default": 0 } + ] + }, + "simpleTypes": { + "enum": [ + "array", + "boolean", + "integer", + "null", + "number", + "object", + "string" + ] + }, + "stringArray": { + "type": "array", + "items": { "type": "string" }, + "uniqueItems": true, + "default": [] + } + }, + "type": ["object", "boolean"], + "properties": { + "$id": { + "type": "string", + "format": "uri-reference" + }, + "$schema": { + "type": "string", + "format": "uri" + }, + "$ref": { + "type": "string", + "format": "uri-reference" + }, + "$comment": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "default": true, + "readOnly": { + "type": "boolean", + "default": false + }, + "writeOnly": { + "type": "boolean", + "default": false + }, + "examples": { + "type": "array", + "items": true + }, + "multipleOf": { + "type": "number", + "exclusiveMinimum": 0 + }, + "maximum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "number" + }, + "minimum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "number" + }, + "maxLength": { "$ref": "#/definitions/nonNegativeInteger" }, + "minLength": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "pattern": { + "type": "string", + "format": "regex" + }, + "additionalItems": { "$ref": "#" }, + "items": { + "anyOf": [ + { "$ref": "#" }, + { "$ref": "#/definitions/schemaArray" } + ], + "default": true + }, + "maxItems": { "$ref": "#/definitions/nonNegativeInteger" }, + "minItems": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "contains": { "$ref": "#" }, + "maxProperties": { "$ref": "#/definitions/nonNegativeInteger" }, + "minProperties": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "required": { "$ref": "#/definitions/stringArray" }, + "additionalProperties": { "$ref": "#" }, + "definitions": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "properties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "patternProperties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "propertyNames": { "format": "regex" }, + "default": {} + }, + "dependencies": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { "$ref": "#" }, + { "$ref": "#/definitions/stringArray" } + ] + } + }, + "propertyNames": { "$ref": "#" }, + "const": true, + "enum": { + "type": "array", + "items": true, + "minItems": 1, + "uniqueItems": true + }, + "type": { + "anyOf": [ + { "$ref": "#/definitions/simpleTypes" }, + { + "type": "array", + "items": { "$ref": "#/definitions/simpleTypes" }, + "minItems": 1, + "uniqueItems": true + } + ] + }, + "format": { "type": "string" }, + "contentMediaType": { "type": "string" }, + "contentEncoding": { "type": "string" }, + "if": { "$ref": "#" }, + "then": { "$ref": "#" }, + "else": { "$ref": "#" }, + "allOf": { "$ref": "#/definitions/schemaArray" }, + "anyOf": { "$ref": "#/definitions/schemaArray" }, + "oneOf": { "$ref": "#/definitions/schemaArray" }, + "not": { "$ref": "#" } + }, + "default": true +} diff --git a/vcr/pe/schema/v2/presentation-definition-claim-format-designations.json b/vcr/pe/schema/v2/presentation-definition-claim-format-designations.json new file mode 100644 index 0000000000..d142274c92 --- /dev/null +++ b/vcr/pe/schema/v2/presentation-definition-claim-format-designations.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Presentation Definition Claim Format Designations", + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^jwt$|^jwt_vc$|^jwt_vp$": { + "type": "object", + "additionalProperties": false, + "properties": { + "alg": { + "type": "array", + "minItems": 1, + "items": { "type": "string" } + } + } + }, + "^ldp_vc$|^ldp_vp$|^ldp$": { + "type": "object", + "additionalProperties": false, + "properties": { + "proof_type": { + "type": "array", + "minItems": 1, + "items": { "type": "string" } + } + } + } + } +} \ No newline at end of file diff --git a/vcr/pe/schema/v2/presentation-definition-envelope.json b/vcr/pe/schema/v2/presentation-definition-envelope.json new file mode 100644 index 0000000000..872a1e3966 --- /dev/null +++ b/vcr/pe/schema/v2/presentation-definition-envelope.json @@ -0,0 +1,353 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Presentation Definition Envelope", + "definitions": { + "status_directive": { + "type": "object", + "additionalProperties": false, + "properties": { + "directive": { + "type": "string", + "enum": [ + "required", + "allowed", + "disallowed" + ] + }, + "type": { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + } + }, + "field": { + "type": "object", + "oneOf": [ + { + "properties": { + "id": { + "type": "string" + }, + "optional": { + "type": "boolean" + }, + "path": { + "type": "array", + "items": { + "type": "string" + } + }, + "purpose": { + "type": "string" + }, + "name": { + "type": "string" + }, + "intent_to_retain": { + "type": "boolean" + }, + "filter": { + "$ref": "http://json-schema.org/draft-07/schema#" + } + }, + "required": [ + "path" + ], + "additionalProperties": false + }, + { + "properties": { + "id": { + "type": "string" + }, + "optional": { + "type": "boolean" + }, + "path": { + "type": "array", + "items": { + "type": "string" + } + }, + "purpose": { + "type": "string" + }, + "intent_to_retain": { + "type": "boolean" + }, + "filter": { + "$ref": "http://json-schema.org/draft-07/schema#" + }, + "name": { + "type": "string" + }, + "predicate": { + "type": "string", + "enum": [ + "required", + "preferred" + ] + } + }, + "required": [ + "path", + "filter", + "predicate" + ], + "additionalProperties": false + } + ] + }, + "input_descriptor": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "purpose": { + "type": "string" + }, + "format": { + "$ref": "http://identity.foundation/claim-format-registry/schemas/presentation-definition-claim-format-designations.json" + }, + "group": { + "type": "array", + "items": { + "type": "string" + } + }, + "constraints": { + "type": "object", + "additionalProperties": false, + "properties": { + "limit_disclosure": { + "type": "string", + "enum": [ + "required", + "preferred" + ] + }, + "statuses": { + "type": "object", + "additionalProperties": false, + "properties": { + "active": { + "$ref": "#/definitions/status_directive" + }, + "suspended": { + "$ref": "#/definitions/status_directive" + }, + "revoked": { + "$ref": "#/definitions/status_directive" + } + } + }, + "fields": { + "type": "array", + "items": { + "$ref": "#/definitions/field" + } + }, + "subject_is_issuer": { + "type": "string", + "enum": [ + "required", + "preferred" + ] + }, + "is_holder": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "field_id": { + "type": "array", + "items": { + "type": "string" + } + }, + "directive": { + "type": "string", + "enum": [ + "required", + "preferred" + ] + } + }, + "required": [ + "field_id", + "directive" + ] + } + }, + "same_subject": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "field_id": { + "type": "array", + "items": { + "type": "string" + } + }, + "directive": { + "type": "string", + "enum": [ + "required", + "preferred" + ] + } + }, + "required": [ + "field_id", + "directive" + ] + } + } + } + } + }, + "required": [ + "id", + "constraints" + ] + }, + "submission_requirement": { + "type": "object", + "oneOf": [ + { + "properties": { + "name": { + "type": "string" + }, + "purpose": { + "type": "string" + }, + "rule": { + "type": "string", + "enum": [ + "all", + "pick" + ] + }, + "count": { + "type": "integer", + "minimum": 1 + }, + "min": { + "type": "integer", + "minimum": 0 + }, + "max": { + "type": "integer", + "minimum": 0 + }, + "from": { + "type": "string" + } + }, + "required": [ + "rule", + "from" + ], + "additionalProperties": false + }, + { + "properties": { + "name": { + "type": "string" + }, + "purpose": { + "type": "string" + }, + "rule": { + "type": "string", + "enum": [ + "all", + "pick" + ] + }, + "count": { + "type": "integer", + "minimum": 1 + }, + "min": { + "type": "integer", + "minimum": 0 + }, + "max": { + "type": "integer", + "minimum": 0 + }, + "from_nested": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/submission_requirement" + } + } + }, + "required": [ + "rule", + "from_nested" + ], + "additionalProperties": false + } + ] + }, + "presentation_definition": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "purpose": { + "type": "string" + }, + "format": { + "$ref": "http://identity.foundation/claim-format-registry/schemas/presentation-definition-claim-format-designations.json#" + }, + "frame": { + "type": "object", + "additionalProperties": true + }, + "submission_requirements": { + "type": "array", + "items": { + "$ref": "#/definitions/submission_requirement" + } + }, + "input_descriptors": { + "type": "array", + "items": { + "$ref": "#/definitions/input_descriptor" + } + } + }, + "required": [ + "id", + "input_descriptors" + ], + "additionalProperties": false + } + }, + "type": "object", + "properties": { + "presentation_definition": { + "$ref": "#/definitions/presentation_definition" + } + } +} diff --git a/vcr/pe/schema/v2/presentation-definition.json b/vcr/pe/schema/v2/presentation-definition.json new file mode 100644 index 0000000000..7ba8262b5c --- /dev/null +++ b/vcr/pe/schema/v2/presentation-definition.json @@ -0,0 +1,345 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Presentation Definition", + "definitions": { + "status_directive": { + "type": "object", + "additionalProperties": false, + "properties": { + "directive": { + "type": "string", + "enum": [ + "required", + "allowed", + "disallowed" + ] + }, + "type": { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + } + }, + "field": { + "type": "object", + "oneOf": [ + { + "properties": { + "id": { + "type": "string" + }, + "optional": { + "type": "boolean" + }, + "path": { + "type": "array", + "items": { + "type": "string" + } + }, + "purpose": { + "type": "string" + }, + "name": { + "type": "string" + }, + "intent_to_retain": { + "type": "boolean" + }, + "filter": { + "$ref": "http://json-schema.org/draft-07/schema#" + } + }, + "required": [ + "path" + ], + "additionalProperties": false + }, + { + "properties": { + "id": { + "type": "string" + }, + "optional": { + "type": "boolean" + }, + "path": { + "type": "array", + "items": { + "type": "string" + } + }, + "purpose": { + "type": "string" + }, + "intent_to_retain": { + "type": "boolean" + }, + "filter": { + "$ref": "http://json-schema.org/draft-07/schema#" + }, + "name": { + "type": "string" + }, + "predicate": { + "type": "string", + "enum": [ + "required", + "preferred" + ] + } + }, + "required": [ + "path", + "filter", + "predicate" + ], + "additionalProperties": false + } + ] + }, + "input_descriptor": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "purpose": { + "type": "string" + }, + "format": { + "$ref": "http://identity.foundation/claim-format-registry/schemas/presentation-definition-claim-format-designations.json" + }, + "group": { + "type": "array", + "items": { + "type": "string" + } + }, + "constraints": { + "type": "object", + "additionalProperties": false, + "properties": { + "limit_disclosure": { + "type": "string", + "enum": [ + "required", + "preferred" + ] + }, + "statuses": { + "type": "object", + "additionalProperties": false, + "properties": { + "active": { + "$ref": "#/definitions/status_directive" + }, + "suspended": { + "$ref": "#/definitions/status_directive" + }, + "revoked": { + "$ref": "#/definitions/status_directive" + } + } + }, + "fields": { + "type": "array", + "items": { + "$ref": "#/definitions/field" + } + }, + "subject_is_issuer": { + "type": "string", + "enum": [ + "required", + "preferred" + ] + }, + "is_holder": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "field_id": { + "type": "array", + "items": { + "type": "string" + } + }, + "directive": { + "type": "string", + "enum": [ + "required", + "preferred" + ] + } + }, + "required": [ + "field_id", + "directive" + ] + } + }, + "same_subject": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "field_id": { + "type": "array", + "items": { + "type": "string" + } + }, + "directive": { + "type": "string", + "enum": [ + "required", + "preferred" + ] + } + }, + "required": [ + "field_id", + "directive" + ] + } + } + } + } + }, + "required": [ + "id", + "constraints" + ] + }, + "submission_requirement": { + "type": "object", + "oneOf": [ + { + "properties": { + "name": { + "type": "string" + }, + "purpose": { + "type": "string" + }, + "rule": { + "type": "string", + "enum": [ + "all", + "pick" + ] + }, + "count": { + "type": "integer", + "minimum": 1 + }, + "min": { + "type": "integer", + "minimum": 0 + }, + "max": { + "type": "integer", + "minimum": 0 + }, + "from": { + "type": "string" + } + }, + "required": [ + "rule", + "from" + ], + "additionalProperties": false + }, + { + "properties": { + "name": { + "type": "string" + }, + "purpose": { + "type": "string" + }, + "rule": { + "type": "string", + "enum": [ + "all", + "pick" + ] + }, + "count": { + "type": "integer", + "minimum": 1 + }, + "min": { + "type": "integer", + "minimum": 0 + }, + "max": { + "type": "integer", + "minimum": 0 + }, + "from_nested": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/submission_requirement" + } + } + }, + "required": [ + "rule", + "from_nested" + ], + "additionalProperties": false + } + ] + } + }, + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "purpose": { + "type": "string" + }, + "format": { + "$ref": "http://identity.foundation/claim-format-registry/schemas/presentation-definition-claim-format-designations.json" + }, + "frame": { + "type": "object", + "additionalProperties": true + }, + "submission_requirements": { + "type": "array", + "items": { + "$ref": "#/definitions/submission_requirement" + } + }, + "input_descriptors": { + "type": "array", + "items": { + "$ref": "#/definitions/input_descriptor" + } + } + }, + "required": [ + "id", + "input_descriptors" + ], + "additionalProperties": false +} \ No newline at end of file diff --git a/vcr/pe/schema/v2/presentation-submission-claim-format-designations.json b/vcr/pe/schema/v2/presentation-submission-claim-format-designations.json new file mode 100644 index 0000000000..929f5360fc --- /dev/null +++ b/vcr/pe/schema/v2/presentation-submission-claim-format-designations.json @@ -0,0 +1,11 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Presentation Submission Claim Format Designations", + "type": "object", + "definitions": { + "format": { + "type": "string", + "enum": ["jwt", "jwt_vc", "jwt_vp", "ldp", "ldp_vc", "ldp_vp"] + } + } +} \ No newline at end of file diff --git a/vcr/pe/schema/v2/presentation-submission.json b/vcr/pe/schema/v2/presentation-submission.json new file mode 100644 index 0000000000..a97275528f --- /dev/null +++ b/vcr/pe/schema/v2/presentation-submission.json @@ -0,0 +1,40 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Presentation Submission", + "type": "object", + "properties": { + "presentation_submission": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "definition_id": { "type": "string" }, + "descriptor_map": { + "type": "array", + "items": { "$ref": "#/definitions/descriptor" } + } + }, + "required": ["id", "definition_id", "descriptor_map"], + "additionalProperties": false + } + }, + "definitions": { + "descriptor": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "path": { "type": "string" }, + "path_nested": { + "type": "object", + "$ref": "#/definitions/descriptor" + }, + "format": { + "$ref": "http://identity.foundation/claim-format-registry/schemas/presentation-submission-claim-format-designations.json#/definitions/format" + } + }, + "required": ["id", "path", "format"], + "additionalProperties": false + } + }, + "required": ["presentation_submission"], + "additionalProperties": false +} \ No newline at end of file diff --git a/vcr/pe/schema/v2/schema.go b/vcr/pe/schema/v2/schema.go new file mode 100644 index 0000000000..44de55fdd8 --- /dev/null +++ b/vcr/pe/schema/v2/schema.go @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2023 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 v2 implements v2.0.0 of the Presentation Exchange specification +package v2 + +import ( + "bytes" + "embed" + _ "embed" + "fmt" + "github.com/santhosh-tekuri/jsonschema" + "github.com/santhosh-tekuri/jsonschema/loader" + "io" + "io/fs" + "strings" +) + +const ( + inputDescriptor = "http://identity.foundation/presentation-exchange/schemas/input-descriptor.json" + presentationDefinitionEnvelope = "http://identity.foundation/presentation-exchange/schemas/presentation-definition-envelope.json" + presentationDefinition = "http://identity.foundation/presentation-exchange/schemas/presentation-definition.json" + presentationSubmission = "http://identity.foundation/presentation-exchange/schemas/presentation-submission.json" + submissionRequirement = "http://identity.foundation/presentation-exchange/schemas/submission-requirement.json" + submissionRequirements = "http://identity.foundation/presentation-exchange/schemas/submission-requirements.json" + presentationSubmissionClaimFormatDesignations = "http://identity.foundation/claim-format-registry/schemas/presentation-submission-claim-format-designations.json" + presentationDefinitionClaimFormatDesignations = "http://identity.foundation/claim-format-registry/schemas/presentation-definition-claim-format-designations.json" +) + +//go:embed *.json +var schemaFiles embed.FS + +// PresentationDefinition is the JSON schema for a presentation definition. +var PresentationDefinition *jsonschema.Schema + +// PresentationSubmission is the JSON schema for a presentation submission. +var PresentationSubmission *jsonschema.Schema + +func init() { + // By default, it loads from filesystem, but that sounds unsafe. + // Since register our schemas, we don't need to allow loading resources. + loader.Load = func(url string) (io.ReadCloser, error) { + return nil, fmt.Errorf("refusing to load unknown schema: %s", url) + } + compiler := jsonschema.NewCompiler() + compiler.Draft = jsonschema.Draft7 + if err := loadSchemas(schemaFiles, compiler); err != nil { + panic(err) + } + PresentationDefinition = compiler.MustCompile(presentationDefinition) + PresentationSubmission = compiler.MustCompile(presentationSubmission) +} + +func loadSchemas(reader fs.ReadFileFS, compiler *jsonschema.Compiler) error { + var resources = map[string]string{ + "http://json-schema.org/draft-07/schema": "json-schema-draft-07.json", + } + schemaURLs := []string{ + inputDescriptor, + presentationDefinitionEnvelope, + presentationDefinition, + presentationSubmission, + submissionRequirement, + submissionRequirements, + presentationSubmissionClaimFormatDesignations, + presentationDefinitionClaimFormatDesignations, + } + for _, schemaURL := range schemaURLs { + // Last part of schema URL matches the embedded file's name + parts := strings.Split(schemaURL, "/") + fileName := parts[len(parts)-1] + resources[schemaURL] = fileName + } + for schemaURL, fileName := range resources { + data, err := reader.ReadFile(fileName) + if err != nil { + return fmt.Errorf("error reading schema file %s: %w", fileName, err) + } + if err := compiler.AddResource(schemaURL, bytes.NewReader(data)); err != nil { + return fmt.Errorf("error compiling schema %s: %w", schemaURL, err) + } + } + return nil +} + +// Validate validates the given data against the given schema. +func Validate(data []byte, schema *jsonschema.Schema) error { + return schema.Validate(bytes.NewReader(data)) +} diff --git a/vcr/pe/schema/v2/schema_test.go b/vcr/pe/schema/v2/schema_test.go new file mode 100644 index 0000000000..fa58d5939f --- /dev/null +++ b/vcr/pe/schema/v2/schema_test.go @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2023 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 v2 + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestSchemaLoading(t *testing.T) { + assert.NotNil(t, PresentationDefinition) +} + +func TestValidate(t *testing.T) { + t.Run("ok", func(t *testing.T) { + err := Validate([]byte(`{"id":"1", "input_descriptors": []}`), PresentationDefinition) + assert.NoError(t, err) + }) + t.Run("invalid", func(t *testing.T) { + err := Validate([]byte(`{}`), PresentationDefinition) + assert.ErrorContains(t, err, "doesn't validate") + assert.ErrorContains(t, err, "missing properties: \"id\"") + }) +} diff --git a/vcr/pe/schema/v2/submission-requirement.json b/vcr/pe/schema/v2/submission-requirement.json new file mode 100644 index 0000000000..9650087ef1 --- /dev/null +++ b/vcr/pe/schema/v2/submission-requirement.json @@ -0,0 +1,50 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Presentation Submission Requirement", + "definitions": { + "submission_requirement": { + "type": "object", + "oneOf": [ + { + "properties": { + "name": { "type": "string" }, + "purpose": { "type": "string" }, + "rule": { + "type": "string", + "enum": ["all", "pick"] + }, + "count": { "type": "integer", "minimum": 1 }, + "min": { "type": "integer", "minimum": 0 }, + "max": { "type": "integer", "minimum": 0 }, + "from": { "type": "string" } + }, + "required": ["rule", "from"], + "additionalProperties": false + }, + { + "properties": { + "name": { "type": "string" }, + "purpose": { "type": "string" }, + "rule": { + "type": "string", + "enum": ["all", "pick"] + }, + "count": { "type": "integer", "minimum": 1 }, + "min": { "type": "integer", "minimum": 0 }, + "max": { "type": "integer", "minimum": 0 }, + "from_nested": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/submission_requirement" + } + } + }, + "required": ["rule", "from_nested"], + "additionalProperties": false + } + ] + } + }, + "$ref": "#/definitions/submission_requirement" +} \ No newline at end of file diff --git a/vcr/pe/schema/v2/submission-requirements.json b/vcr/pe/schema/v2/submission-requirements.json new file mode 100644 index 0000000000..c04701eb81 --- /dev/null +++ b/vcr/pe/schema/v2/submission-requirements.json @@ -0,0 +1,102 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Submission Requirements", + "definitions": { + "submission_requirements": { + "type": "object", + "oneOf": [ + { + "properties": { + "name": { + "type": "string" + }, + "purpose": { + "type": "string" + }, + "rule": { + "type": "string", + "enum": [ + "all", + "pick" + ] + }, + "count": { + "type": "integer", + "minimum": 1 + }, + "min": { + "type": "integer", + "minimum": 0 + }, + "max": { + "type": "integer", + "minimum": 0 + }, + "from": { + "type": "string" + } + }, + "required": [ + "rule", + "from" + ], + "additionalProperties": false + }, + { + "properties": { + "name": { + "type": "string" + }, + "purpose": { + "type": "string" + }, + "rule": { + "type": "string", + "enum": [ + "all", + "pick" + ] + }, + "count": { + "type": "integer", + "minimum": 1 + }, + "min": { + "type": "integer", + "minimum": 0 + }, + "max": { + "type": "integer", + "minimum": 0 + }, + "from_nested": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/submission_requirements" + } + } + }, + "required": [ + "rule", + "from_nested" + ], + "additionalProperties": false + } + ] + } + }, + "type": "object", + "properties": { + "submission_requirements": { + "type": "array", + "items": { + "$ref": "#/definitions/submission_requirements" + } + } + }, + "required": [ + "submission_requirements" + ], + "additionalProperties": false +} \ No newline at end of file diff --git a/vcr/pe/store.go b/vcr/pe/store.go index 3c6858687e..11343cd326 100644 --- a/vcr/pe/store.go +++ b/vcr/pe/store.go @@ -20,6 +20,8 @@ package pe import ( "encoding/json" + "fmt" + v2 "github.com/nuts-foundation/nuts-node/vcr/pe/schema/v2" "io" "os" ) @@ -28,7 +30,7 @@ import ( // It loads a file with the mapping from oauth scope to presentation definition type DefinitionResolver struct { // mapping holds the oauth scope to presentation definition mapping - mapping map[string]PresentationDefinition + mapping map[string]validatingPresentationDefinition } // LoadFromFile loads the mapping from the given file @@ -45,8 +47,13 @@ func (s *DefinitionResolver) LoadFromFile(filename string) error { } // unmarshal the bytes into the mapping - s.mapping = make(map[string]PresentationDefinition) - return json.Unmarshal(bytes, &s.mapping) + result := make(map[string]validatingPresentationDefinition) + err = json.Unmarshal(bytes, &result) + if err != nil { + return fmt.Errorf("failed to unmarshal Presentation Exchange mapping file %s: %w", filename, err) + } + s.mapping = result + return nil } // ByScope returns the presentation definition for the given scope. @@ -56,5 +63,16 @@ func (s *DefinitionResolver) ByScope(scope string) *PresentationDefinition { if !ok { return nil } - return &mapping + result := PresentationDefinition(mapping) + return &result +} + +// validatingPresentationDefinition is an alias for PresentationDefinition that validates the JSON on unmarshal. +type validatingPresentationDefinition PresentationDefinition + +func (v *validatingPresentationDefinition) UnmarshalJSON(data []byte) error { + if err := v2.Validate(data, v2.PresentationDefinition); err != nil { + return err + } + return json.Unmarshal(data, (*PresentationDefinition)(v)) } diff --git a/vcr/pe/store_test.go b/vcr/pe/store_test.go index 97e77bdf3f..ebb9ae40cb 100644 --- a/vcr/pe/store_test.go +++ b/vcr/pe/store_test.go @@ -43,6 +43,14 @@ func TestStore_LoadFromFile(t *testing.T) { assert.Error(t, err) }) + + t.Run("returns an error if a presentation definition is invalid", func(t *testing.T) { + store := DefinitionResolver{} + + err := store.LoadFromFile("test/invalid_definition_mapping.json") + + assert.ErrorContains(t, err, "missing properties: \"input_descriptors\"") + }) } func TestStore_ByScope(t *testing.T) { diff --git a/vcr/pe/submission.go b/vcr/pe/submission.go new file mode 100644 index 0000000000..736c70c96c --- /dev/null +++ b/vcr/pe/submission.go @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2023 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 pe + +import ( + "encoding/json" + v2 "github.com/nuts-foundation/nuts-node/vcr/pe/schema/v2" +) + +// PresentationSubmission describes how the VCs in the VP match the input descriptors in the PD +type PresentationSubmission struct { + // Id is the id of the presentation submission, which is a UUID + Id string `json:"id"` + // DefinitionId is the id of the presentation definition that this submission is for + DefinitionId string `json:"definition_id"` + // DescriptorMap is a list of mappings from input descriptors to VCs + DescriptorMap []InputDescriptorMappingObject `json:"descriptor_map"` +} + +// InputDescriptorMappingObject +type InputDescriptorMappingObject struct { + Id string `json:"id"` + Path string `json:"path"` + Format string `json:"format"` +} + +// ParsePresentationSubmission validates the given JSON and parses it into a PresentationSubmission. +// It returns an error if the JSON is invalid or doesn't match the JSON schema for a PresentationSubmission. +func ParsePresentationSubmission(raw []byte) (*PresentationSubmission, error) { + enveloped := `{"presentation_submission":` + string(raw) + `}` + if err := v2.Validate([]byte(enveloped), v2.PresentationSubmission); err != nil { + return nil, err + } + var result PresentationSubmission + err := json.Unmarshal(raw, &result) + if err != nil { + return nil, err + } + return &result, nil +} diff --git a/vcr/pe/submission_test.go b/vcr/pe/submission_test.go new file mode 100644 index 0000000000..1cd9aefb21 --- /dev/null +++ b/vcr/pe/submission_test.go @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2023 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 pe + +import ( + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" +) + +func TestParsePresentationSubmission(t *testing.T) { + t.Run("ok", func(t *testing.T) { + submission, err := ParsePresentationSubmission([]byte(`{"id": "1", "definition_id":"1", "descriptor_map": []}`)) + require.NoError(t, err) + assert.Equal(t, "1", submission.Id) + }) + t.Run("missing id", func(t *testing.T) { + _, err := ParsePresentationSubmission([]byte(`{"definition_id":"1", "descriptor_map": []}`)) + assert.ErrorContains(t, err, `missing properties: "id"`) + }) +} diff --git a/vcr/pe/test/definition_mapping.json b/vcr/pe/test/definition_mapping.json index 284a1253e7..b543faa577 100644 --- a/vcr/pe/test/definition_mapping.json +++ b/vcr/pe/test/definition_mapping.json @@ -1,13 +1,16 @@ { "eOverdracht-overdrachtsbericht": { - "ldp_vc": { - "proof_type": ["JsonWebSignature2020"] + "format": { + "ldp_vc": { + "proof_type": ["JsonWebSignature2020"] + } }, "id": "pd_any_care_organization", "name": "Care organization", "purpose": "Finding a care organization for authorizing access to medical metadata", "input_descriptors": [ { + "id": "id_nuts_care_organization_cred", "constraints": { "fields": [ { diff --git a/vcr/pe/test/invalid_definition_mapping.json b/vcr/pe/test/invalid_definition_mapping.json new file mode 100644 index 0000000000..cfceb8bed5 --- /dev/null +++ b/vcr/pe/test/invalid_definition_mapping.json @@ -0,0 +1,5 @@ +{ + "missing input_descriptors": { + "id": "pd_any_care_organization" + } +} diff --git a/vcr/pe/types.go b/vcr/pe/types.go index 9dea0b1a96..32f4d78a2f 100644 --- a/vcr/pe/types.go +++ b/vcr/pe/types.go @@ -22,23 +22,6 @@ package pe // PresentationDefinitionClaimFormatDesignations (replaces generated one) type PresentationDefinitionClaimFormatDesignations map[string]map[string][]string -// PresentationSubmission describes how the VCs in the VP match the input descriptors in the PD -type PresentationSubmission struct { - // Id is the id of the presentation submission, which is a UUID - Id string `json:"id"` - // DefinitionId is the id of the presentation definition that this submission is for - DefinitionId string `json:"definition_id"` - // DescriptorMap is a list of mappings from input descriptors to VCs - DescriptorMap []InputDescriptorMappingObject `json:"descriptor_map"` -} - -// InputDescriptorMappingObject -type InputDescriptorMappingObject struct { - Id string `json:"id"` - Path string `json:"path"` - Format string `json:"format"` -} - // Constraints type Constraints struct { Fields []Field `json:"fields,omitempty"` From 9ed1889ef6049dffe8c8e17f1ace7ea5f9136f59 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Oct 2023 15:00:02 +0200 Subject: [PATCH 22/23] Bump go.uber.org/goleak from 1.2.1 to 1.3.0 (#2562) Bumps [go.uber.org/goleak](https://github.com/uber-go/goleak) from 1.2.1 to 1.3.0. - [Release notes](https://github.com/uber-go/goleak/releases) - [Changelog](https://github.com/uber-go/goleak/blob/master/CHANGELOG.md) - [Commits](https://github.com/uber-go/goleak/compare/v1.2.1...v1.3.0) --- updated-dependencies: - dependency-name: go.uber.org/goleak dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 64d8932617..480aa31554 100644 --- a/go.mod +++ b/go.mod @@ -40,7 +40,7 @@ require ( github.com/twmb/murmur3 v1.1.8 go.etcd.io/bbolt v1.3.7 go.uber.org/atomic v1.11.0 - go.uber.org/goleak v1.2.1 + go.uber.org/goleak v1.3.0 go.uber.org/mock v0.3.0 golang.org/x/crypto v0.14.0 golang.org/x/time v0.3.0 diff --git a/go.sum b/go.sum index 72a174816e..875a707ce3 100644 --- a/go.sum +++ b/go.sum @@ -615,8 +615,8 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.uber.org/atomic v1.7.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= -go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= -go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo= go.uber.org/mock v0.3.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= From db42d9720eefd54d0174bd489c40d79d6991e8c4 Mon Sep 17 00:00:00 2001 From: Gerard Snaauw <33763579+gerardsn@users.noreply.github.com> Date: Wed, 25 Oct 2023 15:08:15 +0200 Subject: [PATCH 23/23] Refactor ParsePublicURL (#2560) * refactor ParsePublicURL * add tests --- auth/api/iam/metadata.go | 8 +------- auth/auth.go | 6 +----- core/url.go | 13 +++++++++++-- core/url_test.go | 30 +++++++++++++++++++++++++++++- network/transport/types.go | 2 +- 5 files changed, 43 insertions(+), 16 deletions(-) diff --git a/auth/api/iam/metadata.go b/auth/api/iam/metadata.go index 21977ea927..cf4f193ea3 100644 --- a/auth/api/iam/metadata.go +++ b/auth/api/iam/metadata.go @@ -36,13 +36,7 @@ const ( // IssuerIdToWellKnown converts the OAuth2 Issuer identity to the specified well-known endpoint by inserting the well-known at the root of the path. // It returns no url and an error when issuer is not a valid URL. func IssuerIdToWellKnown(issuer string, wellKnown string, strictmode bool) (*url.URL, error) { - var issuerURL *url.URL - var err error - if strictmode { - issuerURL, err = core.ParsePublicURL(issuer, false, "https") - } else { - issuerURL, err = core.ParsePublicURL(issuer, true, "https", "http") - } + issuerURL, err := core.ParsePublicURL(issuer, strictmode) if err != nil { return nil, err } diff --git a/auth/auth.go b/auth/auth.go index 62e010a726..0db58e7484 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -128,11 +128,7 @@ func (auth *Auth) Configure(config core.ServerConfig) error { return errors.New("invalid auth.publicurl: must provide url") } var err error - if config.Strictmode { - auth.publicURL, err = core.ParsePublicURL(auth.config.PublicURL, false, "https") - } else { - auth.publicURL, err = core.ParsePublicURL(auth.config.PublicURL, true, "http", "https") - } + auth.publicURL, err = core.ParsePublicURL(auth.config.PublicURL, config.Strictmode) if err != nil { return fmt.Errorf("invalid auth.publicurl: %w", err) } diff --git a/core/url.go b/core/url.go index 52e836f368..e0322cd61e 100644 --- a/core/url.go +++ b/core/url.go @@ -43,11 +43,20 @@ func JoinURLPaths(parts ...string) string { return result } -// ParsePublicURL parses the given input string as URL and asserts that +// ParsePublicURL parses the input URL using ParsePublicURLWithScheme. +// If strictmode is true, no reserved addresses are allowed and the scheme MUST be 'https' +func ParsePublicURL(input string, strictmode bool) (*url.URL, error) { + if !strictmode { + return ParsePublicURLWithScheme(input, true, "http", "https") + } + return ParsePublicURLWithScheme(input, false, "https") +} + +// ParsePublicURLWithScheme parses the given input string as URL and asserts that // it has a scheme and that it is in the allowedSchemes if provided, // it is not an IP address, and // it is not (depending on allowReserved) a reserved address or TLD as described in RFC2606 or https://www.ietf.org/archive/id/draft-chapin-rfc2606bis-00.html. -func ParsePublicURL(input string, allowReserved bool, allowedSchemes ...string) (*url.URL, error) { +func ParsePublicURLWithScheme(input string, allowReserved bool, allowedSchemes ...string) (*url.URL, error) { parsed, err := url.Parse(input) if err != nil { return nil, err diff --git a/core/url_test.go b/core/url_test.go index bc68f3ffc7..1ee1cbb3b2 100644 --- a/core/url_test.go +++ b/core/url_test.go @@ -35,6 +35,34 @@ func TestJoinURLPaths(t *testing.T) { } func Test_ParsePublicURL(t *testing.T) { + t.Run("ok - strict", func(t *testing.T) { + u, err := ParsePublicURL("https://non.reserved", true) + require.NoError(t, err) + assert.Equal(t, "https://non.reserved", u.String()) + }) + t.Run("error - strict - scheme must be https", func(t *testing.T) { + u, err := ParsePublicURL("http://localhost", true) + assert.Nil(t, u) + assert.EqualError(t, err, "scheme must be https") + }) + t.Run("error - strict - reserved address", func(t *testing.T) { + u, err := ParsePublicURL("https://localhost", true) + assert.Nil(t, u) + assert.EqualError(t, err, "hostname is RFC2606 reserved") + }) + t.Run("error - strict - IP address", func(t *testing.T) { + u, err := ParsePublicURL("https://127.0.0.1", true) + assert.Nil(t, u) + assert.EqualError(t, err, "hostname is IP") + }) + t.Run("ok - non-strict", func(t *testing.T) { + u, err := ParsePublicURL("http://localhost", false) + require.NoError(t, err) + assert.Equal(t, "http://localhost", u.String()) + }) +} + +func Test_ParsePublicURLWithScheme(t *testing.T) { errIncompleteURL := errors.New("url must contain scheme and host") errIsIpAddress := errors.New("hostname is IP") errIsReserved := errors.New("hostname is RFC2606 reserved") @@ -64,7 +92,7 @@ func Test_ParsePublicURL(t *testing.T) { } for _, tc := range tests { - addr, err := ParsePublicURL(tc.input, tc.allowReserved, "http", "https", "grpc") + addr, err := ParsePublicURLWithScheme(tc.input, tc.allowReserved, "http", "https", "grpc") if tc.err == nil { // valid test cases require.NoError(t, err, "test case: %v", tc) diff --git a/network/transport/types.go b/network/transport/types.go index f7dc2c96a8..6ea2c7c48c 100644 --- a/network/transport/types.go +++ b/network/transport/types.go @@ -144,7 +144,7 @@ func (s *NutsCommURL) UnmarshalJSON(bytes []byte) error { if err := json.Unmarshal(bytes, &str); err != nil { return errors.New("endpoint not a string") } - endpoint, err := core.ParsePublicURL(str, false, "grpc") + endpoint, err := core.ParsePublicURLWithScheme(str, false, "grpc") if err != nil { return err }