diff --git a/Dockerfile b/Dockerfile index 7d11f6822c..f3d581caba 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # golang alpine -FROM golang:1.21.6-alpine as builder +FROM golang:1.22.0-alpine as builder ARG TARGETARCH ARG TARGETOS diff --git a/discovery/client.go b/discovery/client.go index f1aff6e6a6..9ef8a153d5 100644 --- a/discovery/client.go +++ b/discovery/client.go @@ -24,7 +24,6 @@ import ( "fmt" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/go-did/vc" - "github.com/nuts-foundation/nuts-node/audit" nutsCrypto "github.com/nuts-foundation/nuts-node/crypto" "github.com/nuts-foundation/nuts-node/discovery/api/v1/client" "github.com/nuts-foundation/nuts-node/discovery/log" @@ -35,14 +34,12 @@ import ( ) // clientRegistrationManager is a client component, responsible for managing registrations on a Discovery Service. -// It automatically refreshes registered Verifiable Presentations when they are about to expire. +// It can refresh registered Verifiable Presentations when they are about to expire. type clientRegistrationManager interface { activate(ctx context.Context, serviceID string, subjectDID did.DID) error deactivate(ctx context.Context, serviceID string, subjectDID did.DID) error - // refresh is a blocking call to periodically refresh registrations. - // It checks for Verifiable Presentations that are about to expire, and should be refreshed on the Discovery Service. - // It will exit when the given context is cancelled. - refresh(ctx context.Context, interval time.Duration) + // refresh checks which Verifiable Presentations that are about to expire, and should be refreshed on the Discovery Service. + refresh(ctx context.Context, now time.Time) error } var _ clientRegistrationManager = &defaultClientRegistrationManager{} @@ -77,7 +74,7 @@ func (r *defaultClientRegistrationManager) activate(ctx context.Context, service err := r.registerPresentation(ctx, subjectDID, service) if err != nil { // failed, will be retried on next scheduled refresh - return errors.Join(ErrPresentationRegistrationFailed, err) + return fmt.Errorf("%w: %w", ErrPresentationRegistrationFailed, err) } log.Logger().Debugf("Successfully registered Verifiable Presentation on Discovery Service (service=%s, did=%s)", serviceID, subjectDID) @@ -102,7 +99,7 @@ func (r *defaultClientRegistrationManager) deactivate(ctx context.Context, servi "credentialSubject.id": subjectDID.String(), }) if err != nil { - return errors.Join(ErrPresentationRegistrationFailed, err) + return fmt.Errorf("%w: %w", ErrPresentationRegistrationFailed, err) } if len(presentations) == 0 { // no registration, nothing to do @@ -114,11 +111,11 @@ func (r *defaultClientRegistrationManager) deactivate(ctx context.Context, servi "retract_jti": presentations[0].ID.String(), }) if err != nil { - return errors.Join(ErrPresentationRegistrationFailed, err) + return fmt.Errorf("%w: %w", ErrPresentationRegistrationFailed, err) } err = r.client.Register(ctx, service.Endpoint, *presentation) if err != nil { - return errors.Join(ErrPresentationRegistrationFailed, err) + return fmt.Errorf("%w: %w", ErrPresentationRegistrationFailed, err) } return nil } @@ -163,41 +160,22 @@ func (r *defaultClientRegistrationManager) buildPresentation(ctx context.Context }, &subjectDID, false) } -func (r *defaultClientRegistrationManager) doRefresh(ctx context.Context, now time.Time) error { +func (r *defaultClientRegistrationManager) refresh(ctx context.Context, now time.Time) error { log.Logger().Debug("Refreshing own registered Verifiable Presentations on Discovery Services") serviceIDs, dids, err := r.store.getPresentationsToBeRefreshed(now) if err != nil { return err } + var result error = nil for i, serviceID := range serviceIDs { if err := r.activate(ctx, serviceID, dids[i]); err != nil { - log.Logger().WithError(err).Warnf("Failed to refresh Verifiable Presentation (service=%s, did=%s)", serviceID, dids[i]) + result = errors.Join(result, fmt.Errorf("failed to refresh Verifiable Presentation (service=%s, did=%s): %w", serviceID, dids[i], err)) } } - return nil + return result } -func (r *defaultClientRegistrationManager) refresh(ctx context.Context, interval time.Duration) { - ticker := time.NewTicker(interval) - defer ticker.Stop() - // do the first refresh immediately - do := func() { - if err := r.doRefresh(audit.Context(ctx, "app", ModuleName, "RefreshVerifiablePresentations"), time.Now()); err != nil { - log.Logger().WithError(err).Errorf("Failed to refresh Verifiable Presentations") - } - } - do() - for { - select { - case <-ctx.Done(): - return - case <-ticker.C: - do() - } - } -} - -// clientUpdater is responsible for updating the presentations for the given services, at the given interval. +// clientUpdater is responsible for updating the local copy of Discovery Services // Callers should only call update(). type clientUpdater struct { services map[string]ServiceDefinition @@ -215,29 +193,15 @@ func newClientUpdater(services map[string]ServiceDefinition, store *sqlStore, ve } } -// update starts a blocking loop that updates the presentations for the given services, at the given interval. -// It returns when the context is cancelled. -func (u *clientUpdater) update(ctx context.Context, interval time.Duration) { - ticker := time.NewTicker(interval) - defer ticker.Stop() - u.doUpdate(ctx) - for { - select { - case <-ctx.Done(): - return - case <-ticker.C: - u.doUpdate(ctx) - } - } -} - -func (u *clientUpdater) doUpdate(ctx context.Context) { +func (u *clientUpdater) update(ctx context.Context) error { log.Logger().Debug("Checking for new Verifiable Presentations from Discovery Services") + var result error = nil for _, service := range u.services { if err := u.updateService(ctx, service); err != nil { - log.Logger().Errorf("Failed to update service (id=%s): %s", service.ID, err) + result = errors.Join(result, err) } } + return result } func (u *clientUpdater) updateService(ctx context.Context, service ServiceDefinition) error { diff --git a/discovery/client_test.go b/discovery/client_test.go index d4254dd4c5..0ef2729a3e 100644 --- a/discovery/client_test.go +++ b/discovery/client_test.go @@ -30,7 +30,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" - "sync" "testing" "time" ) @@ -150,7 +149,7 @@ func Test_scheduledRegistrationManager_deregister(t *testing.T) { }) } -func Test_scheduledRegistrationManager_doRefreshRegistrations(t *testing.T) { +func Test_scheduledRegistrationManager_refresh(t *testing.T) { storageEngine := storage.NewTestStorageEngine(t) require.NoError(t, storageEngine.Start()) @@ -161,7 +160,7 @@ func Test_scheduledRegistrationManager_doRefreshRegistrations(t *testing.T) { store := setupStore(t, storageEngine.GetSQLDatabase()) manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR) - err := manager.doRefresh(audit.TestContext(), time.Now()) + err := manager.refresh(audit.TestContext(), time.Now()) require.NoError(t, err) }) @@ -186,37 +185,9 @@ func Test_scheduledRegistrationManager_doRefreshRegistrations(t *testing.T) { wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), &bobDID, false).Return(&vpBob, nil) wallet.EXPECT().List(gomock.Any(), bobDID).Return([]vc.VerifiableCredential{vcBob}, nil) - err := manager.doRefresh(audit.TestContext(), time.Now()) + err := manager.refresh(audit.TestContext(), time.Now()) - require.NoError(t, err) - }) -} - -func Test_scheduledRegistrationManager_refreshRegistrations(t *testing.T) { - storageEngine := storage.NewTestStorageEngine(t) - require.NoError(t, storageEngine.Start()) - - t.Run("context cancel stops the loop", func(t *testing.T) { - store := setupStore(t, storageEngine.GetSQLDatabase()) - ctrl := gomock.NewController(t) - invoker := client.NewMockHTTPClient(ctrl) - mockVCR := vcr.NewMockVCR(ctrl) - wallet := holder.NewMockWallet(ctrl) - mockVCR.EXPECT().Wallet().Return(wallet).AnyTimes() - manager := newRegistrationManager(testDefinitions(), store, invoker, mockVCR) - - ctx, cancel := context.WithCancel(context.Background()) - wg := &sync.WaitGroup{} - wg.Add(1) - go func() { - defer wg.Done() - manager.refresh(ctx, time.Millisecond) - }() - // make sure the loop has at least once - time.Sleep(5 * time.Millisecond) - // Make sure the function exits when the context is cancelled - cancel() - wg.Wait() + assert.EqualError(t, err, "failed to refresh Verifiable Presentation (service=usecase_v1, did=did:example:alice): registration of Verifiable Presentation on remote Discovery Service failed: remote error") }) } @@ -294,42 +265,32 @@ func Test_clientUpdater_updateService(t *testing.T) { } func Test_clientUpdater_update(t *testing.T) { - storageEngine := storage.NewTestStorageEngine(t) - require.NoError(t, storageEngine.Start()) - t.Run("context cancel stops the loop", func(t *testing.T) { + t.Run("proceeds when service update fails", func(t *testing.T) { + storageEngine := storage.NewTestStorageEngine(t) + require.NoError(t, storageEngine.Start()) store := setupStore(t, storageEngine.GetSQLDatabase()) ctrl := gomock.NewController(t) httpClient := client.NewMockHTTPClient(ctrl) - httpClient.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return([]vc.VerifiablePresentation{}, "test", nil).MinTimes(1) + httpClient.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return([]vc.VerifiablePresentation{}, "test", nil) + httpClient.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, "", errors.New("test")) updater := newClientUpdater(testDefinitions(), store, alwaysOkVerifier, httpClient) - ctx, cancel := context.WithCancel(context.Background()) - wg := &sync.WaitGroup{} - wg.Add(1) - go func() { - defer wg.Done() - updater.update(ctx, time.Millisecond) - }() - // make sure the loop has at least once - time.Sleep(5 * time.Millisecond) - // Make sure the function exits when the context is cancelled - cancel() - wg.Wait() - }) -} + err := updater.update(context.Background()) -func Test_clientUpdater_doUpdate(t *testing.T) { - t.Run("proceeds when service update fails", func(t *testing.T) { + require.EqualError(t, err, "failed to get presentations from discovery service (id=other): test") + }) + t.Run("no error", func(t *testing.T) { storageEngine := storage.NewTestStorageEngine(t) require.NoError(t, storageEngine.Start()) store := setupStore(t, storageEngine.GetSQLDatabase()) ctrl := gomock.NewController(t) httpClient := client.NewMockHTTPClient(ctrl) - httpClient.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return([]vc.VerifiablePresentation{}, "test", nil) - httpClient.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, "", errors.New("test")) + httpClient.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return([]vc.VerifiablePresentation{}, "test", nil).MinTimes(2) updater := newClientUpdater(testDefinitions(), store, alwaysOkVerifier, httpClient) - updater.doUpdate(context.Background()) + err := updater.update(context.Background()) + + assert.NoError(t, err) }) } diff --git a/discovery/cmd/cmd.go b/discovery/cmd/cmd.go index 8afb2880bb..4c9bf613c8 100644 --- a/discovery/cmd/cmd.go +++ b/discovery/cmd/cmd.go @@ -33,11 +33,10 @@ func FlagSet() *pflag.FlagSet { flagSet.StringSlice("discovery.server.definition_ids", defs.Server.DefinitionIDs, "IDs of the Discovery Service Definitions for which to act as server. "+ "If an ID does not map to a loaded service definition, the node will fail to start.") - flagSet.Duration("discovery.client.registration_refresh_interval", defs.Client.RegistrationRefreshInterval, - "Interval at which the client should refresh checks for registrations to refresh on the configured Discovery Services,"+ - "in Golang time.Duration string format (e.g. 1s). "+ - "Note that it only will actually refresh registrations that about to expire (less than 1/4th of their lifetime left).") - flagSet.Duration("discovery.client.refresh_interval", defs.Client.RefreshInterval, "How often to check for new Verifiable Presentations on the Discovery Services to update the local copy. "+ - "Specified as Golang duration (e.g. 1m, 1h30m).") + flagSet.Duration("discovery.client.refresh_interval", defs.Client.RefreshInterval, + "Interval at which the client synchronizes with the Discovery Server; "+ + "refreshing Verifiable Presentations of local DIDs and loading changes, updating the local copy. "+ + "It only will actually refresh registrations of local DIDs that about to expire (less than 1/4th of their lifetime left). "+ + "Specified as Golang duration (e.g. 1m, 1h30m).") return flagSet } diff --git a/discovery/config.go b/discovery/config.go index 526f690537..16843525ff 100644 --- a/discovery/config.go +++ b/discovery/config.go @@ -42,9 +42,6 @@ type ServerConfig struct { type ClientConfig struct { // RefreshInterval specifies how often the client should refresh the Discovery Services. RefreshInterval time.Duration `koanf:"refresh_interval"` - // RegistrationRefreshInterval specifies how often the client should refresh its registrations on Discovery Services. - // At the same interval, failed registrations are refreshed. - RegistrationRefreshInterval time.Duration `koanf:"registration_refresh_interval"` } // DefaultConfig returns the default configuration. @@ -52,8 +49,7 @@ func DefaultConfig() Config { return Config{ Server: ServerConfig{}, Client: ClientConfig{ - RefreshInterval: 10 * time.Minute, - RegistrationRefreshInterval: 10 * time.Minute, + RefreshInterval: 10 * time.Minute, }, } } diff --git a/discovery/config_test.go b/discovery/config_test.go index 643f485c4a..c5bc6f31d5 100644 --- a/discovery/config_test.go +++ b/discovery/config_test.go @@ -24,5 +24,5 @@ import ( ) func TestDefaultConfig(t *testing.T) { - assert.NotEmpty(t, DefaultConfig().Client.RegistrationRefreshInterval) + assert.NotEmpty(t, DefaultConfig().Client.RefreshInterval) } diff --git a/discovery/module.go b/discovery/module.go index bce91965d7..eec55d5b7e 100644 --- a/discovery/module.go +++ b/discovery/module.go @@ -25,6 +25,7 @@ import ( ssi "github.com/nuts-foundation/go-did" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/go-did/vc" + "github.com/nuts-foundation/nuts-node/audit" "github.com/nuts-foundation/nuts-node/core" "github.com/nuts-foundation/nuts-node/discovery/api/v1/client" "github.com/nuts-foundation/nuts-node/discovery/log" @@ -128,17 +129,14 @@ func (m *Module) Start() error { return err } m.clientUpdater = newClientUpdater(m.allDefinitions, m.store, m.verifyRegistration, m.httpClient) - m.routines.Add(1) - go func() { - defer m.routines.Done() - m.clientUpdater.update(m.ctx, m.config.Client.RefreshInterval) - }() m.registrationManager = newRegistrationManager(m.allDefinitions, m.store, m.httpClient, m.vcrInstance) - m.routines.Add(1) - go func() { - defer m.routines.Done() - m.registrationManager.refresh(m.ctx, m.config.Client.RegistrationRefreshInterval) - }() + if m.config.Client.RefreshInterval > 0 { + m.routines.Add(1) + go func() { + defer m.routines.Done() + m.update() + }() + } return nil } @@ -288,6 +286,7 @@ func (m *Module) ActivateServiceForDID(ctx context.Context, serviceID string, su log.Logger().WithError(err).Warnf("Presentation registration failed, will be retried later (did=%s,service=%s)", subjectDID, serviceID) } else if err == nil { log.Logger().Infof("Successfully activated service for DID (did=%s,service=%s)", subjectDID, serviceID) + _ = m.clientUpdater.updateService(ctx, m.allDefinitions[serviceID]) } return err } @@ -394,6 +393,32 @@ func (m *Module) Search(serviceID string, query map[string]string) ([]SearchResu return result, nil } +func (m *Module) update() { + ticker := time.NewTicker(m.config.Client.RefreshInterval) + defer ticker.Stop() + ctx := audit.Context(m.ctx, "app", ModuleName, "RefreshDiscoveryClient") + do := func() { + // Refresh registrations first, to make sure we have (our own) latest presentations when we load them from the Discovery Service + err := m.registrationManager.refresh(ctx, time.Now()) + if err != nil { + log.Logger().WithError(err).Errorf("Failed to refresh own Verifiable Presentations on Discovery Service") + } + err = m.clientUpdater.update(m.ctx) + if err != nil { + log.Logger().WithError(err).Errorf("Failed to load latest Verifiable Presentations from Discovery Service") + } + } + do() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + do() + } + } +} + // validateAudience checks if the given audience of the presentation matches the service ID. func validateAudience(service ServiceDefinition, audience []string) error { for _, audienceID := range audience { diff --git a/discovery/module_test.go b/discovery/module_test.go index 108f4132d5..a3b64333dd 100644 --- a/discovery/module_test.go +++ b/discovery/module_test.go @@ -330,7 +330,58 @@ func TestModule_Search(t *testing.T) { }) } +func TestModule_update(t *testing.T) { + storageEngine := storage.NewTestStorageEngine(t) + require.NoError(t, storageEngine.Start()) + t.Run("Start() initiates update", func(t *testing.T) { + _, _, _ = setupModule(t, storageEngine, func(module *Module) { + // we want to assert the job runs, so make it run very often to make the test faster + module.config.Client.RefreshInterval = 1 * time.Millisecond + // overwrite httpClient mock for custom behavior assertions (we want to know how often HttpClient.Get() was called) + httpClient := client.NewMockHTTPClient(gomock.NewController(t)) + // Get() should be called at least twice (times the number of Service Definitions), once for the initial run on startup, then again after the refresh interval + httpClient.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, "", nil).MinTimes(2 * len(module.allDefinitions)) + module.httpClient = httpClient + }) + time.Sleep(10 * time.Millisecond) + }) + t.Run("update() runs on node startup", func(t *testing.T) { + _, _, _ = setupModule(t, storageEngine, func(module *Module) { + // we want to assert the job immediately executes on node startup, even if the refresh interval hasn't passed + module.config.Client.RefreshInterval = time.Hour + // overwrite httpClient mock for custom behavior assertions (we want to know how often HttpClient.Get() was called) + httpClient := client.NewMockHTTPClient(gomock.NewController(t)) + // update causes call to HttpClient.Get(), once for each Service Definition + httpClient.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, "", nil).Times(len(module.allDefinitions)) + module.httpClient = httpClient + }) + }) +} + func TestModule_ActivateServiceForDID(t *testing.T) { + t.Run("ok, syncs VPs immediately after registration", func(t *testing.T) { + storageEngine := storage.NewTestStorageEngine(t) + require.NoError(t, storageEngine.Start()) + m, _, documentOwner := setupModule(t, storageEngine, func(module *Module) { + // overwrite httpClient mock for custom behavior assertions (we want to know how often HttpClient.Get() was called) + httpClient := client.NewMockHTTPClient(gomock.NewController(t)) + httpClient.EXPECT().Register(gomock.Any(), gomock.Any(), vpAlice).Return(nil) + httpClient.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, "", nil) + module.httpClient = httpClient + // disable auto-refresh job to have deterministic assertions + module.config.Client.RefreshInterval = 0 + }) + // We expect the client to create 1 VP + wallet := holder.NewMockWallet(gomock.NewController(t)) + m.vcrInstance.(*vcr.MockVCR).EXPECT().Wallet().Return(wallet).MinTimes(1) + wallet.EXPECT().List(gomock.Any(), gomock.Any()).Return([]vc.VerifiableCredential{vcAlice}, nil) + wallet.EXPECT().BuildPresentation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(&vpAlice, nil) + documentOwner.EXPECT().IsOwner(gomock.Any(), aliceDID).Return(true, nil) + + err := m.ActivateServiceForDID(context.Background(), testServiceID, aliceDID) + + assert.NoError(t, err) + }) t.Run("not owned", func(t *testing.T) { storageEngine := storage.NewTestStorageEngine(t) require.NoError(t, storageEngine.Start()) diff --git a/go.mod b/go.mod index 364d836583..dbf015cc17 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,7 @@ require ( github.com/nats-io/nats-server/v2 v2.10.10 github.com/nats-io/nats.go v1.32.0 github.com/nuts-foundation/crypto-ecies v0.0.0-20211207143025-5b84f9efce2b - github.com/nuts-foundation/go-did v0.11.0 + github.com/nuts-foundation/go-did v0.12.0 github.com/nuts-foundation/go-leia/v4 v4.0.1 github.com/nuts-foundation/go-stoabs v1.9.0 // check the oapi-codegen tool version in the makefile when upgrading the runtime @@ -45,7 +45,7 @@ require ( go.uber.org/atomic v1.11.0 go.uber.org/goleak v1.3.0 go.uber.org/mock v0.4.0 - golang.org/x/crypto v0.18.0 + golang.org/x/crypto v0.19.0 golang.org/x/time v0.5.0 google.golang.org/grpc v1.61.0 google.golang.org/protobuf v1.32.0 @@ -161,8 +161,8 @@ require ( github.com/yuin/gopher-lua v1.1.0 // indirect go.opencensus.io v0.24.0 // indirect golang.org/x/net v0.19.0 // indirect - golang.org/x/sys v0.16.0 // indirect - golang.org/x/term v0.16.0 // indirect + golang.org/x/sys v0.17.0 // indirect + golang.org/x/term v0.17.0 // indirect golang.org/x/text v0.14.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 // indirect gopkg.in/Regis24GmbH/go-diacritics.v2 v2.0.3 // indirect @@ -171,7 +171,7 @@ require ( rsc.io/qr v0.2.0 // indirect ) -require gorm.io/driver/postgres v1.5.4 +require gorm.io/driver/postgres v1.5.6 require ( github.com/golang-sql/sqlexp v0.1.0 // indirect diff --git a/go.sum b/go.sum index 7463558930..b9e0f31009 100644 --- a/go.sum +++ b/go.sum @@ -485,8 +485,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.11.0 h1:RTem1MlVoOOoLa/Y2miYRy70Jex0/kJBTCPH5RtUmrY= -github.com/nuts-foundation/go-did v0.11.0/go.mod h1:2e2H2Hqk0SWrrGZEg97dbK/ZFIkkFB65hNWdOSbylrg= +github.com/nuts-foundation/go-did v0.12.0 h1:XmttEpFOxrUXzdXHj2x9h8KlhhPgyr02vgtygWg8xnY= +github.com/nuts-foundation/go-did v0.12.0/go.mod h1:cZiOP2Is9hgIsP5g1FqkfhBDi8f6ktxkP6K4iTX9qns= github.com/nuts-foundation/go-leia/v4 v4.0.1 h1:+Sbk3Bew1QnRUqRXSOwomMw3nIZgncmTX425J7U5Q34= github.com/nuts-foundation/go-leia/v4 v4.0.1/go.mod h1:eaZuWIolpU61TMvTMcen85+SOEOnHiALdg5SxqLXzz8= github.com/nuts-foundation/go-stoabs v1.9.0 h1:zK+ugfolaJYyBvGwsRuavLVdycXk4Yw/1gI+tz17lWQ= @@ -684,8 +684,8 @@ golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58 golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= -golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA= golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= @@ -799,16 +799,17 @@ 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.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= -golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= -golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 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= @@ -909,8 +910,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/mysql v1.5.2 h1:QC2HRskSE75wBuOxe0+iCkyJZ+RqpudsQtqkp+IMuXs= gorm.io/driver/mysql v1.5.2/go.mod h1:pQLhh1Ut/WUAySdTHwBpBv6+JKcj+ua4ZFx1QQTBzb8= -gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo= -gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0= +gorm.io/driver/postgres v1.5.6 h1:ydr9xEd5YAM0vxVDY0X139dyzNz10spDiDlC7+ibLeU= +gorm.io/driver/postgres v1.5.6/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA= gorm.io/driver/sqlite v1.5.5 h1:7MDMtUZhV065SilG62E0MquljeArQZNfJnjd9i9gx3E= gorm.io/driver/sqlite v1.5.5/go.mod h1:6NgQ7sQWAIFsPrJJl1lSNSu2TABh0ZZ/zm5fosATavE= gorm.io/driver/sqlserver v1.5.2 h1:+o4RQ8w1ohPbADhFqDxeeZnSWjwOcBnxBckjTbcP4wk= diff --git a/vdr/resolver/did_test.go b/vdr/resolver/did_test.go index f0b0cbf743..d7b9ddef78 100644 --- a/vdr/resolver/did_test.go +++ b/vdr/resolver/did_test.go @@ -95,8 +95,11 @@ func Test_deactivatedError_Is(t *testing.T) { } func newDidDoc() did.Document { + return newDidDocWithDID(did.MustParseDID("did:example:sakjsakldjsakld")) +} + +func newDidDocWithDID(id did.DID) did.Document { privateKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - id := did.MustParseDID("did:example:sakjsakldjsakld") keyID := did.DIDURL{DID: id} keyID.Fragment = "key-1" vm, _ := did.NewVerificationMethod(keyID, ssi.JsonWebKey2020, id, privateKey.Public()) diff --git a/vdr/resolver/key_test.go b/vdr/resolver/key_test.go index d9db59ea7e..cc7c6ad3cd 100644 --- a/vdr/resolver/key_test.go +++ b/vdr/resolver/key_test.go @@ -23,6 +23,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" + "strings" "testing" ) @@ -40,6 +41,19 @@ func TestKeyResolver_ResolveKey(t *testing.T) { assert.Equal(t, doc.VerificationMethod[0].ID.URI(), keyId) assert.NotNil(t, key) }) + t.Run("ok - did:web with port", func(t *testing.T) { + // This test checks for regression of DID.URI() double-encoding, causing %3A to be encoded to %253A + // This was fixed in go-did v0.12.0 + ctrl := gomock.NewController(t) + resolver := NewMockDIDResolver(ctrl) + keyResolver := DIDKeyResolver{Resolver: resolver} + doc := newDidDocWithDID(did.MustParseDID("did:web:example.com%3A8443")) + resolver.EXPECT().Resolve(doc.ID, gomock.Any()).AnyTimes().Return(&doc, nil, nil) + + keyId, _, err := keyResolver.ResolveKey(doc.ID, nil, AssertionMethod) + require.NoError(t, err) + assert.Truef(t, strings.HasPrefix(keyId.String(), doc.ID.String()), "%s does not start with DID %s", keyId, doc.ID) + }) t.Run("error - document not found", func(t *testing.T) { unknownDID := did.MustParseDID("did:example:123")