Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

allow for additional parameters when activating discovery service #3357

Merged
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions auth/api/iam/generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 2 additions & 30 deletions auth/client/iam/openid4vp.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,8 @@ import (
"context"
"encoding/json"
"fmt"
"github.com/google/uuid"
ssi "github.com/nuts-foundation/go-did"
"github.com/nuts-foundation/nuts-node/http/client"
"github.com/nuts-foundation/nuts-node/vcr/credential"
"github.com/nuts-foundation/nuts-node/vdr/didsubject"
"github.com/piprate/json-gold/ld"
"net/http"
Expand Down Expand Up @@ -263,7 +262,7 @@ func (c *OpenID4VPClient) RequestRFC021AccessToken(ctx context.Context, clientID
additionalCredentials := make(map[did.DID][]vc.VerifiableCredential)
for _, subjectDID := range subjectDIDs {
for _, curr := range credentials {
additionalCredentials[subjectDID] = append(additionalCredentials[subjectDID], autoCorrectSelfAttestedCredential(curr, subjectDID))
additionalCredentials[subjectDID] = append(additionalCredentials[subjectDID], credential.AutoCorrectSelfAttestedCredential(curr, subjectDID))
}
}
vp, submission, err := c.wallet.BuildSubmission(ctx, subjectDIDs, additionalCredentials, *presentationDefinition, metadata.VPFormatsSupported, params)
Expand Down Expand Up @@ -362,30 +361,3 @@ func (c *OpenID4VPClient) dpop(ctx context.Context, requester did.DID, request h
}
return jwt, keyID, nil
}

// autoCorrectSelfAttestedCredential sets the required fields for a self-attested credential.
// These are provided through the API, and for convenience we set the required fields, if not already set.
// It only does this for unsigned credentials.
func autoCorrectSelfAttestedCredential(credential vc.VerifiableCredential, requester did.DID) vc.VerifiableCredential {
if len(credential.Proof) > 0 {
return credential
}
if credential.ID == nil {
credential.ID, _ = ssi.ParseURI(uuid.NewString())
}
if credential.Issuer.String() == "" {
credential.Issuer = requester.URI()
}
if credential.IssuanceDate.IsZero() {
credential.IssuanceDate = time.Now()
}
var credentialSubject []map[string]interface{}
_ = credential.UnmarshalCredentialSubject(&credentialSubject)
if len(credentialSubject) == 1 {
if _, ok := credentialSubject[0]["id"]; !ok {
credentialSubject[0]["id"] = requester.String()
credential.CredentialSubject[0] = credentialSubject[0]
}
}
return credential
}
14 changes: 10 additions & 4 deletions discovery/api/v1/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,10 @@ func (w *Wrapper) SearchPresentations(ctx context.Context, request SearchPresent
results := make([]SearchResult, 0)
for _, searchResult := range searchResults {
result := SearchResult{
Vp: searchResult.Presentation,
Id: searchResult.Presentation.ID.String(),
Fields: searchResult.Fields,
Vp: searchResult.Presentation,
Id: searchResult.Presentation.ID.String(),
Fields: searchResult.Fields,
RegistrationParameters: searchResult.Parameters,
}
subjectDID, _ := credential.PresentationSigner(searchResult.Presentation)
if subjectDID != nil {
Expand All @@ -101,7 +102,12 @@ func (w *Wrapper) SearchPresentations(ctx context.Context, request SearchPresent
}

func (w *Wrapper) ActivateServiceForSubject(ctx context.Context, request ActivateServiceForSubjectRequestObject) (ActivateServiceForSubjectResponseObject, error) {
err := w.Client.ActivateServiceForSubject(ctx, request.ServiceID, request.SubjectID)
var parameters map[string]interface{}
if request.Body != nil && request.Body.RegistrationParameters != nil {
parameters = *request.Body.RegistrationParameters
}

err := w.Client.ActivateServiceForSubject(ctx, request.ServiceID, request.SubjectID, parameters)
if errors.Is(err, discovery.ErrPresentationRegistrationFailed) {
// registration failed, but will be retried
return ActivateServiceForSubject202JSONResponse{
Expand Down
26 changes: 23 additions & 3 deletions discovery/api/v1/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const (
func TestWrapper_ActivateServiceForSubject(t *testing.T) {
t.Run("ok", func(t *testing.T) {
test := newMockContext(t)
test.client.EXPECT().ActivateServiceForSubject(gomock.Any(), serviceID, subjectID).Return(nil)
test.client.EXPECT().ActivateServiceForSubject(gomock.Any(), serviceID, subjectID, nil).Return(nil)

response, err := test.wrapper.ActivateServiceForSubject(nil, ActivateServiceForSubjectRequestObject{
ServiceID: serviceID,
Expand All @@ -53,9 +53,27 @@ func TestWrapper_ActivateServiceForSubject(t *testing.T) {
assert.NoError(t, err)
assert.IsType(t, ActivateServiceForSubject200Response{}, response)
})
t.Run("ok with params", func(t *testing.T) {
test := newMockContext(t)
parameters := map[string]interface{}{
"foo": "bar",
}
test.client.EXPECT().ActivateServiceForSubject(gomock.Any(), serviceID, subjectID, parameters).Return(nil)

response, err := test.wrapper.ActivateServiceForSubject(nil, ActivateServiceForSubjectRequestObject{
ServiceID: serviceID,
SubjectID: subjectID,
Body: &ActivateServiceForSubjectJSONRequestBody{
RegistrationParameters: &parameters,
},
})

assert.NoError(t, err)
assert.IsType(t, ActivateServiceForSubject200Response{}, response)
})
t.Run("ok, but registration failed", func(t *testing.T) {
test := newMockContext(t)
test.client.EXPECT().ActivateServiceForSubject(gomock.Any(), gomock.Any(), gomock.Any()).Return(discovery.ErrPresentationRegistrationFailed)
test.client.EXPECT().ActivateServiceForSubject(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(discovery.ErrPresentationRegistrationFailed)

response, err := test.wrapper.ActivateServiceForSubject(nil, ActivateServiceForSubjectRequestObject{
ServiceID: serviceID,
Expand All @@ -67,7 +85,7 @@ func TestWrapper_ActivateServiceForSubject(t *testing.T) {
})
t.Run("other error", func(t *testing.T) {
test := newMockContext(t)
test.client.EXPECT().ActivateServiceForSubject(gomock.Any(), gomock.Any(), gomock.Any()).Return(errors.New("foo"))
test.client.EXPECT().ActivateServiceForSubject(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(errors.New("foo"))

_, err := test.wrapper.ActivateServiceForSubject(nil, ActivateServiceForSubjectRequestObject{
ServiceID: serviceID,
Expand Down Expand Up @@ -125,6 +143,7 @@ func TestWrapper_SearchPresentations(t *testing.T) {
{
Presentation: vp,
Fields: nil,
Parameters: map[string]interface{}{"test": "value"},
},
}
test.client.EXPECT().Search(serviceID, expectedQuery).Return(results, nil)
Expand All @@ -140,6 +159,7 @@ func TestWrapper_SearchPresentations(t *testing.T) {
assert.Equal(t, vp, actual[0].Vp)
assert.Equal(t, vp.ID.String(), actual[0].Id)
assert.Equal(t, "did:nuts:foo", actual[0].CredentialSubjectId)
assert.Equal(t, "value", actual[0].RegistrationParameters["test"])
})
t.Run("no results", func(t *testing.T) {
test := newMockContext(t)
Expand Down
28 changes: 26 additions & 2 deletions discovery/api/v1/generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions discovery/api/v1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,6 @@ type VerifiablePresentation = vc.VerifiablePresentation

// ServiceDefinition is a type alias
type ServiceDefinition = discovery.ServiceDefinition

// VerifiableCredential is a type alias for the VerifiableCredential from the go-did library.
type VerifiableCredential = vc.VerifiableCredential
36 changes: 25 additions & 11 deletions discovery/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@ import (
"context"
"errors"
"fmt"
ssi "github.com/nuts-foundation/go-did"
"github.com/nuts-foundation/go-did/did"
"github.com/nuts-foundation/go-did/vc"
nutsCrypto "github.com/nuts-foundation/nuts-node/crypto"
"github.com/nuts-foundation/nuts-node/discovery/api/server/client"
"github.com/nuts-foundation/nuts-node/discovery/log"
"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/signature/proof"
"github.com/nuts-foundation/nuts-node/vdr/didsubject"
Expand All @@ -38,7 +40,7 @@ import (
// clientRegistrationManager is a client component, responsible for managing registrations on a Discovery Service.
// It can refresh registered Verifiable Presentations when they are about to expire.
type clientRegistrationManager interface {
activate(ctx context.Context, serviceID, subjectID string) error
activate(ctx context.Context, serviceID, subjectID string, parameters map[string]interface{}) error
deactivate(ctx context.Context, serviceID, subjectID string) error
// 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
Expand All @@ -64,7 +66,7 @@ func newRegistrationManager(services map[string]ServiceDefinition, store *sqlSto
}
}

func (r *defaultClientRegistrationManager) activate(ctx context.Context, serviceID, subjectID string) error {
func (r *defaultClientRegistrationManager) activate(ctx context.Context, serviceID, subjectID string, parameters map[string]interface{}) error {
service, serviceExists := r.services[serviceID]
if !serviceExists {
return ErrServiceNotFound
Expand All @@ -74,15 +76,15 @@ func (r *defaultClientRegistrationManager) activate(ctx context.Context, service
return err
}
var asSoonAsPossible time.Time
if err := r.store.updatePresentationRefreshTime(serviceID, subjectID, &asSoonAsPossible); err != nil {
if err := r.store.updatePresentationRefreshTime(serviceID, subjectID, parameters, &asSoonAsPossible); err != nil {
return err
}
log.Logger().Debugf("Registering Verifiable Presentation on Discovery Service (service=%s, subject=%s)", service.ID, subjectID)

var registeredDIDs []string
var loopErrs []error
for _, subjectDID := range subjectDIDs {
err := r.registerPresentation(ctx, subjectDID, service)
err := r.registerPresentation(ctx, subjectDID, service, parameters)
if err != nil {
if !errors.Is(err, errMissingCredential) { // ignore missing credentials
loopErrs = append(loopErrs, fmt.Errorf("%s: %w", subjectDID.String(), err))
Expand All @@ -107,15 +109,15 @@ func (r *defaultClientRegistrationManager) activate(ctx context.Context, service
// Set presentation to be refreshed before it expires
// TODO: When to refresh? For now, we refresh when the registration is about at 45% of max age. This means a refresh can fail once without consequence.
refreshVPAfter := time.Now().Add(time.Duration(float64(service.PresentationMaxValidity)*0.45) * time.Second)
if err := r.store.updatePresentationRefreshTime(serviceID, subjectID, &refreshVPAfter); err != nil {
if err := r.store.updatePresentationRefreshTime(serviceID, subjectID, parameters, &refreshVPAfter); err != nil {
return fmt.Errorf("unable to update Verifiable Presentation refresh time: %w", err)
}
return nil
}

func (r *defaultClientRegistrationManager) deactivate(ctx context.Context, serviceID, subjectID string) error {
// delete DID/service combination from DB, so it won't be registered again
err := r.store.updatePresentationRefreshTime(serviceID, subjectID, nil)
err := r.store.updatePresentationRefreshTime(serviceID, subjectID, nil, nil)
if err != nil {
return err
}
Expand Down Expand Up @@ -166,19 +168,29 @@ func (r *defaultClientRegistrationManager) deregisterPresentation(ctx context.Co
return r.client.Register(ctx, service.Endpoint, *presentation)
}

func (r *defaultClientRegistrationManager) registerPresentation(ctx context.Context, subjectDID did.DID, service ServiceDefinition) error {
presentation, err := r.findCredentialsAndBuildPresentation(ctx, subjectDID, service)
func (r *defaultClientRegistrationManager) registerPresentation(ctx context.Context, subjectDID did.DID, service ServiceDefinition, parameters map[string]interface{}) error {
presentation, err := r.findCredentialsAndBuildPresentation(ctx, subjectDID, service, parameters)
if err != nil {
return err
}
return r.client.Register(ctx, service.Endpoint, *presentation)
}

func (r *defaultClientRegistrationManager) findCredentialsAndBuildPresentation(ctx context.Context, subjectDID did.DID, service ServiceDefinition) (*vc.VerifiablePresentation, error) {
func (r *defaultClientRegistrationManager) findCredentialsAndBuildPresentation(ctx context.Context, subjectDID did.DID, service ServiceDefinition, parameters map[string]interface{}) (*vc.VerifiablePresentation, error) {
credentials, err := r.vcr.Wallet().List(ctx, subjectDID)
if err != nil {
return nil, err
}
// add registration params as credential
if len(parameters) > 0 {
registrationCredential := vc.VerifiableCredential{
Context: []ssi.URI{vc.VCContextV1URI(), credential.NutsV1ContextURI},
Type: []ssi.URI{vc.VerifiableCredentialTypeV1URI(), credential.DiscoveryRegistrationCredentialTypeV1URI()},
CredentialSubject: []interface{}{parameters},
}
credentials = append(credentials, credential.AutoCorrectSelfAttestedCredential(registrationCredential, subjectDID))
}

matchingCredentials, _, err := service.PresentationDefinition.Match(credentials)
const errStr = "failed to match Discovery Service's Presentation Definition (service=%s, did=%s): %w"
if err != nil {
Expand All @@ -195,6 +207,7 @@ func (r *defaultClientRegistrationManager) buildPresentation(ctx context.Context
nonce := nutsCrypto.GenerateNonce()
// Make sure the presentation is not valid for longer than the max validity as defined by the Service Definitio.
expires := time.Now().Add(time.Duration(service.PresentationMaxValidity-1) * time.Second).Truncate(time.Second)
holderURI := subjectDID.URI()
return r.vcr.Wallet().BuildPresentation(ctx, credentials, holder.PresentationOptions{
ProofOptions: proof.ProofOptions{
Created: time.Now(),
Expand All @@ -204,6 +217,7 @@ func (r *defaultClientRegistrationManager) buildPresentation(ctx context.Context
AdditionalProperties: additionalProperties,
},
Format: vc.JWTPresentationProofFormat,
Holder: &holderURI,
}, &subjectDID, false)
}

Expand All @@ -215,11 +229,11 @@ func (r *defaultClientRegistrationManager) refresh(ctx context.Context, now time
}
var loopErrs []error
for _, candidate := range refreshCandidates {
if err = r.activate(ctx, candidate.ServiceID, candidate.SubjectID); err != nil {
if err = r.activate(ctx, candidate.ServiceID, candidate.SubjectID, candidate.Parameters); err != nil {
var loopErr error
if errors.Is(err, didsubject.ErrSubjectNotFound) {
// Subject has probably been deactivated. Remove from service or registration will be retried every refresh interval.
err = r.store.updatePresentationRefreshTime(candidate.ServiceID, candidate.SubjectID, nil)
err = r.store.updatePresentationRefreshTime(candidate.ServiceID, candidate.SubjectID, candidate.Parameters, nil)
if err != nil {
loopErr = fmt.Errorf("failed to remove unknown subject (service=%s, subject=%s): %w", candidate.ServiceID, candidate.SubjectID, err)
} else {
Expand Down
Loading
Loading