diff --git a/auth/api/iam/api.go b/auth/api/iam/api.go
index e0d002a280..90e14223c3 100644
--- a/auth/api/iam/api.go
+++ b/auth/api/iam/api.go
@@ -38,6 +38,7 @@ import (
"github.com/nuts-foundation/nuts-node/vdr/resolver"
"html/template"
"net/http"
+ "net/url"
"strings"
"time"
)
@@ -49,6 +50,10 @@ const apiPath = "iam"
const apiModuleName = auth.ModuleName + "/" + apiPath
const httpRequestContextKey = "http-request"
+// accessTokenValidity defines how long access tokens are valid.
+// TODO: Might want to make this configurable at some point
+const accessTokenValidity = 15 * time.Minute
+
//go:embed assets
var assets embed.FS
@@ -76,7 +81,7 @@ func New(authInstance auth.AuthenticationServices, vcrInstance vcr.VCR, vdrInsta
}
}
-func (r Wrapper) Routes(router core.EchoRouter) {
+func (r *Wrapper) Routes(router core.EchoRouter) {
RegisterHandlers(router, NewStrictHandler(r, []StrictMiddlewareFunc{
func(f StrictHandlerFunc, operationID string) StrictHandlerFunc {
return func(ctx echo.Context, request interface{}) (response interface{}, err error) {
@@ -120,7 +125,11 @@ func (r Wrapper) middleware(ctx echo.Context, request interface{}, operationID s
}
// HandleTokenRequest handles calls to the token endpoint for exchanging a grant (e.g authorization code or pre-authorized code) for an access token.
-func (r Wrapper) HandleTokenRequest(_ context.Context, request HandleTokenRequestRequestObject) (HandleTokenRequestResponseObject, error) {
+func (r Wrapper) HandleTokenRequest(ctx context.Context, request HandleTokenRequestRequestObject) (HandleTokenRequestResponseObject, error) {
+ ownDID, err := r.idToOwnedDID(ctx, request.Id)
+ if err != nil {
+ return nil, err
+ }
switch request.Body.GrantType {
case "authorization_code":
// Options:
@@ -130,13 +139,6 @@ func (r Wrapper) HandleTokenRequest(_ context.Context, request HandleTokenReques
Code: oauth.UnsupportedGrantType,
Description: "not implemented yet",
}
- case "vp_token-bearer":
- // Options:
- // - service-to-service vp_token flow
- return nil, oauth.OAuth2Error{
- Code: oauth.UnsupportedGrantType,
- Description: "not implemented yet",
- }
case "urn:ietf:params:oauth:grant-type:pre-authorized_code":
// Options:
// - OpenID4VCI
@@ -144,6 +146,15 @@ func (r Wrapper) HandleTokenRequest(_ context.Context, request HandleTokenReques
Code: oauth.UnsupportedGrantType,
Description: "not implemented yet",
}
+ case "vp_token-bearer":
+ // Nuts RFC021 vp_token bearer flow
+ if request.Body.PresentationSubmission == nil || request.Body.Scope == nil || request.Body.Assertion == nil {
+ return nil, oauth.OAuth2Error{
+ Code: oauth.InvalidRequest,
+ Description: "missing required parameters",
+ }
+ }
+ return r.handleS2SAccessTokenRequest(*ownDID, *request.Body.Scope, *request.Body.PresentationSubmission, *request.Body.Assertion)
default:
return nil, oauth.OAuth2Error{
Code: oauth.UnsupportedGrantType,
@@ -161,7 +172,7 @@ func (r Wrapper) IntrospectAccessToken(ctx context.Context, request IntrospectAc
}
token := AccessToken{}
- if err := r.s2sAccessTokenStore().Get(request.Body.Token, &token); err != nil {
+ if err := r.accessTokenStore().Get(request.Body.Token, &token); err != nil {
// Return 200 + 'Active = false' when token is invalid or malformed
return IntrospectAccessToken200JSONResponse{}, err
}
@@ -232,8 +243,10 @@ func toAnyMap(input any) (*map[string]any, error) {
// HandleAuthorizeRequest handles calls to the authorization endpoint for starting an authorization code flow.
func (r Wrapper) HandleAuthorizeRequest(ctx context.Context, request HandleAuthorizeRequestRequestObject) (HandleAuthorizeRequestResponseObject, error) {
- // TODO: must be web DID once web DID creation and DB are implemented
- ownDID := idToNutsDID(request.Id)
+ ownDID, err := r.idToOwnedDID(ctx, request.Id)
+ if err != nil {
+ return nil, err
+ }
// Create session object to be passed to handler
// Workaround: deepmap codegen doesn't support dynamic query parameters.
@@ -243,7 +256,7 @@ func (r Wrapper) HandleAuthorizeRequest(ctx context.Context, request HandleAutho
for key, value := range httpRequest.URL.Query() {
params[key] = value[0]
}
- session := createSession(params, ownDID)
+ session := createSession(params, *ownDID)
if session.RedirectURI == "" {
// TODO: Spec says that the redirect URI is optional, but it's not clear what to do if it's not provided.
// Threat models say it's unsafe to omit redirect_uri.
@@ -303,7 +316,6 @@ func (r Wrapper) OAuthAuthorizationServerMetadata(ctx context.Context, request O
func (r Wrapper) GetWebDID(_ context.Context, request GetWebDIDRequestObject) (GetWebDIDResponseObject, error) {
ownDID := r.idToDID(request.Id)
-
document, err := r.vdr.ResolveManaged(ownDID)
if err != nil {
if resolver.IsFunctionalResolveError(err) {
@@ -317,20 +329,12 @@ func (r Wrapper) GetWebDID(_ context.Context, request GetWebDIDRequestObject) (G
// OAuthClientMetadata returns the OAuth2 Client metadata for the request.Id if it is managed by this node.
func (r Wrapper) OAuthClientMetadata(ctx context.Context, request OAuthClientMetadataRequestObject) (OAuthClientMetadataResponseObject, error) {
- // TODO: must be web DID once web DID creation and DB are implemented
- ownDID := idToNutsDID(request.Id)
- owned, err := r.vdr.IsOwner(ctx, ownDID)
+ _, err := r.idToOwnedDID(ctx, request.Id)
if err != nil {
- log.Logger().WithField("did", ownDID.String()).Errorf("oauth metadata: failed to assert ownership of did: %s", err.Error())
- return nil, core.Error(500, err.Error())
- }
- if !owned {
- return nil, core.NotFoundError("did not owned")
+ return nil, err
}
- identity := r.auth.PublicURL().JoinPath("iam", request.Id)
-
- return OAuthClientMetadata200JSONResponse(clientMetadata(*identity)), nil
+ return OAuthClientMetadata200JSONResponse(clientMetadata(*r.identityURL(request.Id))), nil
}
func (r Wrapper) PresentationDefinition(_ context.Context, request PresentationDefinitionRequestObject) (PresentationDefinitionResponseObject, error) {
if len(request.Params.Scope) == 0 {
@@ -350,6 +354,27 @@ func (r Wrapper) PresentationDefinition(_ context.Context, request PresentationD
return PresentationDefinition200JSONResponse(*presentationDefinition), nil
}
+func (r Wrapper) idToOwnedDID(ctx context.Context, id string) (*did.DID, error) {
+ ownDID := r.idToDID(id)
+ owned, err := r.vdr.IsOwner(ctx, ownDID)
+ if err != nil {
+ if resolver.IsFunctionalResolveError(err) {
+ return nil, oauth.OAuth2Error{
+ Code: oauth.InvalidRequest,
+ Description: "invalid issuer DID: " + err.Error(),
+ }
+ }
+ return nil, fmt.Errorf("DID resolution failed: %w", err)
+ }
+ if !owned {
+ return nil, oauth.OAuth2Error{
+ Code: oauth.InvalidRequest,
+ Description: "issuer DID not owned by the server",
+ }
+ }
+ return &ownDID, nil
+}
+
func createSession(params map[string]string, ownDID did.DID) *Session {
session := &Session{
// TODO: Validate client ID
@@ -365,17 +390,22 @@ func createSession(params map[string]string, ownDID did.DID) *Session {
}
return session
}
+
+// idToDID converts the tenant-specific part of a did:web DID (e.g. 123)
+// to a fully qualified did:web DID (e.g. did:web:example.com:123), using the configured Nuts node URL.
func (r Wrapper) idToDID(id string) did.DID {
- url := r.auth.PublicURL().JoinPath("iam", id)
- did, _ := didweb.URLToDID(*url)
- return *did
+ identityURL := r.identityURL(id)
+ result, _ := didweb.URLToDID(*identityURL)
+ return *result
}
-func idToNutsDID(id string) did.DID {
- return did.DID{
- // should be changed to web when migrated to web DID
- Method: "nuts",
- ID: id,
- DecodedID: id,
- }
+// identityURL the tenant-specific part of a did:web DID (e.g. 123)
+// to an identity URL (e.g. did:web:example.com:123), which is used as base URL for resolving metadata and its did:web DID,
+// using the configured Nuts node URL.
+func (r Wrapper) identityURL(id string) *url.URL {
+ return r.auth.PublicURL().JoinPath("iam", id)
+}
+
+func (r *Wrapper) accessTokenStore() storage.SessionStore {
+ return r.storageEngine.GetSessionDatabase().GetStore(accessTokenValidity, "accesstoken")
}
diff --git a/auth/api/iam/api_test.go b/auth/api/iam/api_test.go
index 1c322b56fe..4055e479c2 100644
--- a/auth/api/iam/api_test.go
+++ b/auth/api/iam/api_test.go
@@ -37,7 +37,9 @@ import (
"github.com/nuts-foundation/nuts-node/core"
"github.com/nuts-foundation/nuts-node/jsonld"
"github.com/nuts-foundation/nuts-node/storage"
+ "github.com/nuts-foundation/nuts-node/vcr"
"github.com/nuts-foundation/nuts-node/vcr/pe"
+ "github.com/nuts-foundation/nuts-node/vcr/verifier"
"github.com/nuts-foundation/nuts-node/vdr"
"github.com/nuts-foundation/nuts-node/vdr/resolver"
"github.com/stretchr/testify/assert"
@@ -46,7 +48,6 @@ import (
"time"
)
-var nutsDID = did.MustParseDID("did:nuts:123")
var webDID = did.MustParseDID("did:web:example.com:iam:123")
var webIDPart = "123"
@@ -140,32 +141,13 @@ func TestWrapper_GetWebDID(t *testing.T) {
func TestWrapper_GetOAuthClientMetadata(t *testing.T) {
t.Run("ok", func(t *testing.T) {
ctx := newTestClient(t)
- ctx.vdr.EXPECT().IsOwner(nil, nutsDID).Return(true, nil)
+ ctx.vdr.EXPECT().IsOwner(nil, webDID).Return(true, nil)
- res, err := ctx.client.OAuthClientMetadata(nil, OAuthClientMetadataRequestObject{Id: nutsDID.ID})
+ res, err := ctx.client.OAuthClientMetadata(nil, OAuthClientMetadataRequestObject{Id: webIDPart})
require.NoError(t, err)
assert.IsType(t, OAuthClientMetadata200JSONResponse{}, res)
})
- t.Run("error - did not managed by this node", func(t *testing.T) {
- ctx := newTestClient(t)
- ctx.vdr.EXPECT().IsOwner(nil, nutsDID)
-
- res, err := ctx.client.OAuthClientMetadata(nil, OAuthClientMetadataRequestObject{Id: nutsDID.ID})
-
- assert.Equal(t, 404, statusCodeFrom(err))
- assert.Nil(t, res)
- })
- t.Run("error - internal error 500", func(t *testing.T) {
- ctx := newTestClient(t)
- ctx.vdr.EXPECT().IsOwner(nil, nutsDID).Return(false, errors.New("unknown error"))
-
- res, err := ctx.client.OAuthClientMetadata(nil, OAuthClientMetadataRequestObject{Id: nutsDID.ID})
-
- assert.Equal(t, 500, statusCodeFrom(err))
- assert.EqualError(t, err, "unknown error")
- assert.Nil(t, res)
- })
}
func TestWrapper_PresentationDefinition(t *testing.T) {
webDID := did.MustParseDID("did:web:example.com:iam:123")
@@ -175,7 +157,6 @@ func TestWrapper_PresentationDefinition(t *testing.T) {
t.Run("ok", func(t *testing.T) {
test := newTestClient(t)
- test.authnServices.EXPECT().PresentationDefinitions().Return(&definitionResolver)
response, err := test.client.PresentationDefinition(ctx, PresentationDefinitionRequestObject{Did: webDID.ID, Params: PresentationDefinitionParams{Scope: "eOverdracht-overdrachtsbericht"}})
@@ -198,7 +179,6 @@ func TestWrapper_PresentationDefinition(t *testing.T) {
t.Run("error - unknown scope", func(t *testing.T) {
test := newTestClient(t)
- test.authnServices.EXPECT().PresentationDefinitions().Return(&definitionResolver)
response, err := test.client.PresentationDefinition(ctx, PresentationDefinitionRequestObject{Did: webDID.ID, Params: PresentationDefinitionParams{Scope: "unknown"}})
@@ -211,9 +191,10 @@ func TestWrapper_PresentationDefinition(t *testing.T) {
func TestWrapper_HandleAuthorizeRequest(t *testing.T) {
t.Run("missing redirect_uri", func(t *testing.T) {
ctx := newTestClient(t)
+ ctx.vdr.EXPECT().IsOwner(gomock.Any(), webDID).Return(true, nil)
res, err := ctx.client.HandleAuthorizeRequest(requestContext(map[string]string{}), HandleAuthorizeRequestRequestObject{
- Id: nutsDID.String(),
+ Id: webIDPart,
})
requireOAuthError(t, err, oauth.InvalidRequest, "redirect_uri is required")
@@ -221,12 +202,13 @@ func TestWrapper_HandleAuthorizeRequest(t *testing.T) {
})
t.Run("unsupported response type", func(t *testing.T) {
ctx := newTestClient(t)
+ ctx.vdr.EXPECT().IsOwner(gomock.Any(), webDID).Return(true, nil)
res, err := ctx.client.HandleAuthorizeRequest(requestContext(map[string]string{
"redirect_uri": "https://example.com",
"response_type": "unsupported",
}), HandleAuthorizeRequestRequestObject{
- Id: nutsDID.String(),
+ Id: webIDPart,
})
requireOAuthError(t, err, oauth.UnsupportedResponseType, "")
@@ -237,9 +219,10 @@ func TestWrapper_HandleAuthorizeRequest(t *testing.T) {
func TestWrapper_HandleTokenRequest(t *testing.T) {
t.Run("unsupported grant type", func(t *testing.T) {
ctx := newTestClient(t)
+ ctx.vdr.EXPECT().IsOwner(gomock.Any(), webDID).Return(true, nil)
res, err := ctx.client.HandleTokenRequest(nil, HandleTokenRequestRequestObject{
- Id: nutsDID.String(),
+ Id: webIDPart,
Body: &HandleTokenRequestFormdataRequestBody{
GrantType: "unsupported",
},
@@ -267,7 +250,7 @@ func TestWrapper_IntrospectAccessToken(t *testing.T) {
})
t.Run("error - expired token", func(t *testing.T) {
token := AccessToken{Expiration: time.Now().Add(-time.Second)}
- require.NoError(t, ctx.client.s2sAccessTokenStore().Put("token", token))
+ require.NoError(t, ctx.client.accessTokenStore().Put("token", token))
res, err := ctx.client.IntrospectAccessToken(context.Background(), IntrospectAccessTokenRequestObject{Body: &TokenIntrospectionRequest{Token: "token"}})
@@ -276,7 +259,7 @@ func TestWrapper_IntrospectAccessToken(t *testing.T) {
})
t.Run("ok", func(t *testing.T) {
token := AccessToken{Expiration: time.Now().Add(time.Second)}
- require.NoError(t, ctx.client.s2sAccessTokenStore().Put("token", token))
+ require.NoError(t, ctx.client.accessTokenStore().Put("token", token))
res, err := ctx.client.IntrospectAccessToken(context.Background(), IntrospectAccessTokenRequestObject{Body: &TokenIntrospectionRequest{Token: "token"}})
@@ -307,7 +290,7 @@ func TestWrapper_IntrospectAccessToken(t *testing.T) {
PresentationDefinition: &pe.PresentationDefinition{},
}
- require.NoError(t, ctx.client.s2sAccessTokenStore().Put(token.Token, token))
+ require.NoError(t, ctx.client.accessTokenStore().Put(token.Token, token))
expectedResponse, err := json.Marshal(IntrospectAccessToken200JSONResponse{
Active: true,
ClientId: ptrTo("client"),
@@ -372,6 +355,8 @@ type testCtx struct {
vdr *vdr.MockVDR
resolver *resolver.MockDIDResolver
relyingParty *oauthServices.MockRelyingParty
+ verifier *verifier.MockVerifier
+ vcr *vcr.MockVCR
}
func newTestClient(t testing.TB) *testCtx {
@@ -379,14 +364,21 @@ func newTestClient(t testing.TB) *testCtx {
require.NoError(t, err)
ctrl := gomock.NewController(t)
storageEngine := storage.NewTestStorageEngine(t)
+ mockVerifier := verifier.NewMockVerifier(ctrl)
+ mockVCR := vcr.NewMockVCR(ctrl)
+ mockVCR.EXPECT().Verifier().Return(mockVerifier).AnyTimes()
authnServices := auth.NewMockAuthenticationServices(ctrl)
authnServices.EXPECT().PublicURL().Return(publicURL).AnyTimes()
+ authnServices.EXPECT().PresentationDefinitions().Return(pe.TestDefinitionResolver(t)).AnyTimes()
resolver := resolver.NewMockDIDResolver(ctrl)
relyingPary := oauthServices.NewMockRelyingParty(ctrl)
+ verifier := verifier.NewMockVerifier(ctrl)
vdr := vdr.NewMockVDR(ctrl)
+ vcr := vcr.NewMockVCR(ctrl)
authnServices.EXPECT().PublicURL().Return(publicURL).AnyTimes()
authnServices.EXPECT().RelyingParty().Return(relyingPary).AnyTimes()
+ vcr.EXPECT().Verifier().Return(verifier).AnyTimes()
vdr.EXPECT().Resolver().Return(resolver).AnyTimes()
return &testCtx{
@@ -394,9 +386,12 @@ func newTestClient(t testing.TB) *testCtx {
relyingParty: relyingPary,
resolver: resolver,
vdr: vdr,
+ verifier: mockVerifier,
+ vcr: mockVCR,
client: &Wrapper{
auth: authnServices,
vdr: vdr,
+ vcr: mockVCR,
storageEngine: storageEngine,
},
}
@@ -409,7 +404,7 @@ func TestWrapper_Routes(t *testing.T) {
router.EXPECT().GET(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
router.EXPECT().POST(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
- Wrapper{}.Routes(router)
+ (&Wrapper{}).Routes(router)
}
func TestWrapper_middleware(t *testing.T) {
@@ -458,6 +453,41 @@ func TestWrapper_middleware(t *testing.T) {
}
+func TestWrapper_idToOwnedDID(t *testing.T) {
+ t.Run("ok", func(t *testing.T) {
+ ctx := newTestClient(t)
+ ctx.vdr.EXPECT().IsOwner(nil, webDID).Return(true, nil)
+
+ _, err := ctx.client.idToOwnedDID(nil, webIDPart)
+
+ assert.NoError(t, err)
+ })
+ t.Run("error - did not managed by this node", func(t *testing.T) {
+ ctx := newTestClient(t)
+ ctx.vdr.EXPECT().IsOwner(nil, webDID)
+
+ _, err := ctx.client.idToOwnedDID(nil, webIDPart)
+
+ assert.EqualError(t, err, "invalid_request - issuer DID not owned by the server")
+ })
+ t.Run("DID does not exist (functional resolver error)", func(t *testing.T) {
+ ctx := newTestClient(t)
+ ctx.vdr.EXPECT().IsOwner(nil, webDID).Return(false, resolver.ErrNotFound)
+
+ _, err := ctx.client.idToOwnedDID(nil, webIDPart)
+
+ assert.EqualError(t, err, "invalid_request - invalid issuer DID: unable to find the DID document")
+ })
+ t.Run("other resolver error", func(t *testing.T) {
+ ctx := newTestClient(t)
+ ctx.vdr.EXPECT().IsOwner(nil, webDID).Return(false, errors.New("unknown error"))
+
+ _, err := ctx.client.idToOwnedDID(nil, webIDPart)
+
+ assert.EqualError(t, err, "DID resolution failed: unknown error")
+ })
+}
+
type strictServerCallCapturer bool
func (s *strictServerCallCapturer) handle(ctx echo.Context, request interface{}) (response interface{}, err error) {
diff --git a/auth/api/iam/generated.go b/auth/api/iam/generated.go
index 8c0961f8f1..1353107de0 100644
--- a/auth/api/iam/generated.go
+++ b/auth/api/iam/generated.go
@@ -107,9 +107,11 @@ type HandleAuthorizeRequestParams struct {
// HandleTokenRequestFormdataBody defines parameters for HandleTokenRequest.
type HandleTokenRequestFormdataBody struct {
- Code string `form:"code" json:"code"`
- GrantType string `form:"grant_type" json:"grant_type"`
- AdditionalProperties map[string]string `json:"-"`
+ Assertion *string `form:"assertion,omitempty" json:"assertion,omitempty"`
+ Code *string `form:"code,omitempty" json:"code,omitempty"`
+ GrantType string `form:"grant_type" json:"grant_type"`
+ PresentationSubmission *string `form:"presentation_submission,omitempty" json:"presentation_submission,omitempty"`
+ Scope *string `form:"scope,omitempty" json:"scope,omitempty"`
}
// RequestAccessTokenJSONBody defines parameters for RequestAccessToken.
@@ -128,85 +130,6 @@ type IntrospectAccessTokenFormdataRequestBody = TokenIntrospectionRequest
// RequestAccessTokenJSONRequestBody defines body for RequestAccessToken for application/json ContentType.
type RequestAccessTokenJSONRequestBody RequestAccessTokenJSONBody
-// Getter for additional properties for HandleTokenRequestFormdataBody. Returns the specified
-// element and whether it was found
-func (a HandleTokenRequestFormdataBody) Get(fieldName string) (value string, found bool) {
- if a.AdditionalProperties != nil {
- value, found = a.AdditionalProperties[fieldName]
- }
- return
-}
-
-// Setter for additional properties for HandleTokenRequestFormdataBody
-func (a *HandleTokenRequestFormdataBody) Set(fieldName string, value string) {
- if a.AdditionalProperties == nil {
- a.AdditionalProperties = make(map[string]string)
- }
- a.AdditionalProperties[fieldName] = value
-}
-
-// Override default JSON handling for HandleTokenRequestFormdataBody to handle AdditionalProperties
-func (a *HandleTokenRequestFormdataBody) UnmarshalJSON(b []byte) error {
- object := make(map[string]json.RawMessage)
- err := json.Unmarshal(b, &object)
- if err != nil {
- return err
- }
-
- if raw, found := object["code"]; found {
- err = json.Unmarshal(raw, &a.Code)
- if err != nil {
- return fmt.Errorf("error reading 'code': %w", err)
- }
- delete(object, "code")
- }
-
- if raw, found := object["grant_type"]; found {
- err = json.Unmarshal(raw, &a.GrantType)
- if err != nil {
- return fmt.Errorf("error reading 'grant_type': %w", err)
- }
- delete(object, "grant_type")
- }
-
- if len(object) != 0 {
- a.AdditionalProperties = make(map[string]string)
- for fieldName, fieldBuf := range object {
- var fieldVal string
- err := json.Unmarshal(fieldBuf, &fieldVal)
- if err != nil {
- return fmt.Errorf("error unmarshaling field %s: %w", fieldName, err)
- }
- a.AdditionalProperties[fieldName] = fieldVal
- }
- }
- return nil
-}
-
-// Override default JSON handling for HandleTokenRequestFormdataBody to handle AdditionalProperties
-func (a HandleTokenRequestFormdataBody) MarshalJSON() ([]byte, error) {
- var err error
- object := make(map[string]json.RawMessage)
-
- object["code"], err = json.Marshal(a.Code)
- if err != nil {
- return nil, fmt.Errorf("error marshaling 'code': %w", err)
- }
-
- object["grant_type"], err = json.Marshal(a.GrantType)
- if err != nil {
- return nil, fmt.Errorf("error marshaling 'grant_type': %w", err)
- }
-
- for fieldName, field := range a.AdditionalProperties {
- object[fieldName], err = json.Marshal(field)
- if err != nil {
- return nil, fmt.Errorf("error marshaling '%s': %w", fieldName, err)
- }
- }
- return json.Marshal(object)
-}
-
// ServerInterface represents all server handlers.
type ServerInterface interface {
// Get the OAuth2 Authorization Server metadata
diff --git a/auth/api/iam/openid4vp_test.go b/auth/api/iam/openid4vp_test.go
index eb55988dfc..a12a8a578d 100644
--- a/auth/api/iam/openid4vp_test.go
+++ b/auth/api/iam/openid4vp_test.go
@@ -93,14 +93,12 @@ func TestWrapper_handlePresentationRequest(t *testing.T) {
}
t.Run("with scope", func(t *testing.T) {
ctrl := gomock.NewController(t)
- peStore := &pe.DefinitionResolver{}
- require.NoError(t, peStore.LoadFromFile("test/presentation_definition_mapping.json"))
mockVDR := vdr.NewMockVDR(ctrl)
mockVCR := vcr.NewMockVCR(ctrl)
mockWallet := holder.NewMockWallet(ctrl)
mockVCR.EXPECT().Wallet().Return(mockWallet)
mockAuth := auth.NewMockAuthenticationServices(ctrl)
- mockAuth.EXPECT().PresentationDefinitions().Return(peStore)
+ mockAuth.EXPECT().PresentationDefinitions().Return(pe.TestDefinitionResolver(t))
mockWallet.EXPECT().List(gomock.Any(), holderDID).Return(walletCredentials, nil)
mockVDR.EXPECT().IsOwner(gomock.Any(), holderDID).Return(true, nil)
instance := New(mockAuth, mockVCR, mockVDR, storage.NewTestStorageEngine(t))
diff --git a/auth/api/iam/s2s_vptoken.go b/auth/api/iam/s2s_vptoken.go
index 4fb1bd6807..136c490def 100644
--- a/auth/api/iam/s2s_vptoken.go
+++ b/auth/api/iam/s2s_vptoken.go
@@ -22,61 +22,91 @@ import (
"context"
"errors"
"fmt"
- "github.com/nuts-foundation/nuts-node/auth/oauth"
- "github.com/nuts-foundation/nuts-node/crypto"
- "net/http"
"time"
- "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/auth/oauth"
"github.com/nuts-foundation/nuts-node/core"
+ "github.com/nuts-foundation/nuts-node/crypto"
"github.com/nuts-foundation/nuts-node/storage"
+ "github.com/nuts-foundation/nuts-node/vcr/credential"
"github.com/nuts-foundation/nuts-node/vcr/pe"
"github.com/nuts-foundation/nuts-node/vdr/resolver"
)
-// 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,
-// then create a presentation submission given the definition which is posted to the token endpoint as vp_token.
-// The AS then returns an access token with the requested scope.
-// Requires:
-// - GET /presentation_definition?scope=... (returns a presentation definition)
-// - POST /token (with vp_token grant)
-type serviceToService struct {
-}
+// s2sMaxPresentationValidity defines the maximum validity of a presentation.
+// This is to prevent replay attacks. The value is specified by Nuts RFC021, and excludes max. clock skew.
+const s2sMaxPresentationValidity = 5 * time.Second
-func (s serviceToService) Routes(router core.EchoRouter) {
- router.Add("GET", "/public/oauth2/:did/presentation_definition", func(echoCtx echo.Context) error {
- // TODO: Read scope, map to presentation definition, return
- return echoCtx.JSON(http.StatusOK, map[string]string{})
- })
-}
+// s2sMaxClockSkew defines the maximum clock skew between nodes.
+// The value is specified by Nuts RFC021.
+const s2sMaxClockSkew = 5 * time.Second
-func (s serviceToService) validateVPToken(params map[string]string) (string, error) {
- submission := params["presentation_submission"]
- scope := params["scope"]
- vp_token := params["vp_token"]
- if submission == "" || scope == "" || vp_token == "" {
- // TODO: right error response
- return "", errors.New("missing required parameters")
- }
- // TODO: https://github.com/nuts-foundation/nuts-node/issues/2418
- // TODO: verify parameters
- return scope, nil
-}
+// handleS2SAccessTokenRequest handles the /token request with vp_token bearer grant type, intended for service-to-service exchanges.
+// It performs cheap checks first (parameter presence and validity, matching VCs to the presentation definition), then the more expensive ones (checking signatures).
+func (r *Wrapper) handleS2SAccessTokenRequest(issuer did.DID, scope string, submissionJSON string, assertionJSON string) (HandleTokenRequestResponseObject, error) {
+ pexEnvelope, err := pe.ParseEnvelope([]byte(assertionJSON))
+ if err != nil {
+ return nil, oauth.OAuth2Error{
+ Code: oauth.InvalidRequest,
+ Description: "assertion parameter is invalid: " + err.Error(),
+ }
+ }
+
+ submission, err := pe.ParsePresentationSubmission([]byte(submissionJSON))
+ if err != nil {
+ return nil, oauth.OAuth2Error{
+ Code: oauth.InvalidRequest,
+ Description: fmt.Sprintf("invalid presentation submission: %s", err.Error()),
+ }
+ }
+
+ var credentialSubjectID did.DID
+ for _, presentation := range pexEnvelope.Presentations {
+ if err := validateS2SPresentationMaxValidity(presentation); err != nil {
+ return nil, err
+ }
+ if subjectDID, err := validatePresentationSigner(presentation, credentialSubjectID); err != nil {
+ return nil, err
+ } else {
+ credentialSubjectID = *subjectDID
+ }
+ if err := r.validatePresentationAudience(presentation, issuer); err != nil {
+ return nil, err
+ }
+ }
+ credentialMap, definition, err := r.validatePresentationSubmission(scope, submission, pexEnvelope)
+ if err != nil {
+ return nil, err
+ }
+ for _, presentation := range pexEnvelope.Presentations {
+ if err := r.validateS2SPresentationNonce(presentation); err != nil {
+ return nil, err
+ }
+ }
+
+ // Check signatures of VP and VCs. Trust should be established by the Presentation Definition.
+ for _, presentation := range pexEnvelope.Presentations {
+ _, err = r.vcr.Verifier().VerifyVP(presentation, true, true, nil)
+ if err != nil {
+ return nil, oauth.OAuth2Error{
+ Code: oauth.InvalidRequest,
+ Description: "presentation(s) or contained credential(s) are invalid",
+ InternalError: err,
+ }
+ }
+ }
-func (s serviceToService) handleAuthzRequest(_ map[string]string, _ *Session) (*authzResponse, error) {
- // Protocol does not support authorization code flow
- return nil, nil
+ // All OK, allow access
+ response, err := r.createS2SAccessToken(issuer, time.Now(), pexEnvelope.Presentations, *submission, *definition, scope, credentialSubjectID, credentialMap)
+ if err != nil {
+ return nil, err
+ }
+ return HandleTokenRequest200JSONResponse(*response), nil
}
-func (r Wrapper) RequestAccessToken(ctx context.Context, request RequestAccessTokenRequestObject) (RequestAccessTokenResponseObject, error) {
+func (r *Wrapper) RequestAccessToken(ctx context.Context, request RequestAccessTokenRequestObject) (RequestAccessTokenResponseObject, error) {
if request.Body == nil {
// why did oapi-codegen generate a pointer for the body??
return nil, core.InvalidInputError("missing request body")
@@ -115,22 +145,24 @@ func (r Wrapper) RequestAccessToken(ctx context.Context, request RequestAccessTo
return RequestAccessToken200JSONResponse(*tokenResult), nil
}
-func (r Wrapper) createAccessToken(issuer did.DID, issueTime time.Time, presentation vc.VerifiablePresentation, scope string) (*oauth.TokenResponse, error) {
+func (r *Wrapper) createS2SAccessToken(issuer did.DID, issueTime time.Time, presentations []vc.VerifiablePresentation, submission pe.PresentationSubmission, definition PresentationDefinition, scope string, credentialSubjectDID did.DID, credentialMap map[string]vc.VerifiableCredential) (*oauth.TokenResponse, error) {
+ fieldsMap, err := definition.ResolveConstraintsFields(credentialMap)
+ if err != nil {
+ return nil, fmt.Errorf("unable to resolve Presentation Definition Constraints Fields: %w", err)
+ }
accessToken := AccessToken{
- Token: crypto.GenerateNonce(),
- Issuer: issuer.String(),
- // TODO: set ClientId
- ClientId: "",
- IssuedAt: issueTime,
- Expiration: issueTime.Add(accessTokenValidity),
- Scope: scope,
- // TODO: set values
- InputDescriptorConstraintIdMap: nil,
- VPToken: []VerifiablePresentation{presentation},
- PresentationDefinition: nil,
- PresentationSubmission: nil,
- }
- err := r.s2sAccessTokenStore().Put(accessToken.Token, accessToken)
+ Token: crypto.GenerateNonce(),
+ Issuer: issuer.String(),
+ ClientId: credentialSubjectDID.String(),
+ IssuedAt: issueTime,
+ Expiration: issueTime.Add(accessTokenValidity),
+ Scope: scope,
+ VPToken: presentations,
+ PresentationDefinition: &definition,
+ PresentationSubmission: &submission,
+ InputDescriptorConstraintIdMap: fieldsMap,
+ }
+ err = r.accessTokenStore().Put(accessToken.Token, accessToken)
if err != nil {
return nil, fmt.Errorf("unable to store access token: %w", err)
}
@@ -143,8 +175,144 @@ func (r Wrapper) createAccessToken(issuer did.DID, issueTime time.Time, presenta
}, nil
}
-func (r Wrapper) s2sAccessTokenStore() storage.SessionStore {
- return r.storageEngine.GetSessionDatabase().GetStore(accessTokenValidity, "s2s", "accesstoken")
+// validatePresentationSubmission checks if the presentation submission is valid for the given scope:
+// 1. Resolve presentation definition for the requested scope
+// 2. Check submission against presentation and definition
+func (r Wrapper) validatePresentationSubmission(scope string, submission *pe.PresentationSubmission, pexEnvelope *pe.Envelope) (map[string]vc.VerifiableCredential, *PresentationDefinition, error) {
+ definition := r.auth.PresentationDefinitions().ByScope(scope)
+ if definition == nil {
+ return nil, nil, oauth.OAuth2Error{
+ Code: oauth.InvalidScope,
+ Description: fmt.Sprintf("unsupported scope for presentation exchange: %s", scope),
+ }
+ }
+
+ credentialMap, err := submission.Validate(*pexEnvelope, *definition)
+ if err != nil {
+ return nil, nil, oauth.OAuth2Error{
+ Code: oauth.InvalidRequest,
+ Description: "presentation submission does not conform to Presentation Definition",
+ InternalError: err,
+ }
+ }
+ return credentialMap, definition, err
+}
+
+// validateS2SPresentationMaxValidity checks that the presentation is valid for a reasonable amount of time.
+func validateS2SPresentationMaxValidity(presentation vc.VerifiablePresentation) error {
+ created := credential.PresentationIssuanceDate(presentation)
+ expires := credential.PresentationExpirationDate(presentation)
+ if created == nil || expires == nil {
+ return oauth.OAuth2Error{
+ Code: oauth.InvalidRequest,
+ Description: "presentation is missing creation or expiration date",
+ }
+ }
+ if expires.Sub(*created) > s2sMaxPresentationValidity {
+ return oauth.OAuth2Error{
+ Code: oauth.InvalidRequest,
+ Description: fmt.Sprintf("presentation is valid for too long (max %s)", s2sMaxPresentationValidity),
+ }
+ }
+ return nil
+}
+
+// validatePresentationSigner checks if the presenter of the VP is the same as the subject of the VCs being presented.
+func validatePresentationSigner(presentation vc.VerifiablePresentation, expectedCredentialSubjectDID did.DID) (*did.DID, error) {
+ subjectDID, err := credential.PresenterIsCredentialSubject(presentation)
+ if err != nil {
+ return nil, oauth.OAuth2Error{
+ Code: oauth.InvalidRequest,
+ Description: err.Error(),
+ }
+ }
+ if subjectDID == nil {
+ return nil, oauth.OAuth2Error{
+ Code: oauth.InvalidRequest,
+ Description: "presentation signer is not credential subject",
+ }
+ }
+ if !expectedCredentialSubjectDID.Empty() && !subjectDID.Equals(expectedCredentialSubjectDID) {
+ return nil, oauth.OAuth2Error{
+ Code: oauth.InvalidRequest,
+ Description: "not all presentations have the same credential subject ID",
+ }
+ }
+ return subjectDID, nil
+}
+
+// validateS2SPresentationNonce checks if the nonce has been used before; 'nonce' claim for JWTs or LDProof's 'nonce' for JSON-LD.
+func (r *Wrapper) validateS2SPresentationNonce(presentation vc.VerifiablePresentation) error {
+ var nonce string
+ switch presentation.Format() {
+ case vc.JWTPresentationProofFormat:
+ nonceRaw, _ := presentation.JWT().Get("nonce")
+ nonce, _ = nonceRaw.(string)
+ if nonce == "" {
+ return oauth.OAuth2Error{
+ Code: oauth.InvalidRequest,
+ Description: "presentation has invalid/missing nonce",
+ }
+ }
+ case vc.JSONLDPresentationProofFormat:
+ proof, err := credential.ParseLDProof(presentation)
+ if err != nil || proof.Nonce == nil || *proof.Nonce == "" {
+ return oauth.OAuth2Error{
+ Code: oauth.InvalidRequest,
+ InternalError: err,
+ Description: "presentation has invalid proof or nonce",
+ }
+ }
+ nonce = *proof.Nonce
+ }
+
+ nonceStore := r.storageEngine.GetSessionDatabase().GetStore(s2sMaxPresentationValidity+s2sMaxClockSkew, "s2s", "nonce")
+ nonceError := nonceStore.Get(nonce, new(bool))
+ if nonceError != nil && errors.Is(nonceError, storage.ErrNotFound) {
+ // this is OK, nonce has not been used before
+ nonceError = nil
+ } else if nonceError == nil {
+ // no store error: value was retrieved from store, meaning the nonce has been used before
+ nonceError = oauth.OAuth2Error{
+ Code: oauth.InvalidRequest,
+ Description: "presentation nonce has already been used",
+ }
+ }
+ // Other error occurred. Keep error to report after storing nonce.
+
+ // Regardless the result of the nonce checking, the nonce of the VP must not be used again.
+ // So always store the nonce.
+ if err := nonceStore.Put(nonce, true); err != nil {
+ nonceError = errors.Join(fmt.Errorf("unable to store nonce: %w", err), nonceError)
+ }
+ return nonceError
+}
+
+// validatePresentationAudience checks if the presentation audience (aud claim for JWTs, domain property for JSON-LD proofs) contains the issuer DID.
+func (r *Wrapper) validatePresentationAudience(presentation vc.VerifiablePresentation, issuer did.DID) error {
+ var audience []string
+ switch presentation.Format() {
+ case vc.JWTPresentationProofFormat:
+ audience = presentation.JWT().Audience()
+ case vc.JSONLDPresentationProofFormat:
+ proof, err := credential.ParseLDProof(presentation)
+ if err != nil {
+ return err
+ }
+ if proof.Domain != nil {
+ audience = []string{*proof.Domain}
+ }
+ }
+ for _, aud := range audience {
+ if aud == issuer.String() {
+ return nil
+ }
+ }
+ return oauth.OAuth2Error{
+ Code: oauth.InvalidRequest,
+ Description: "presentation audience/domain is missing or does not match",
+ InternalError: fmt.Errorf("expected: %s, got: %v", issuer, audience),
+ }
}
type AccessToken struct {
diff --git a/auth/api/iam/s2s_vptoken_test.go b/auth/api/iam/s2s_vptoken_test.go
index c034a4e505..7ce63422fe 100644
--- a/auth/api/iam/s2s_vptoken_test.go
+++ b/auth/api/iam/s2s_vptoken_test.go
@@ -19,18 +19,30 @@
package iam
import (
+ "github.com/lestrrat-go/jwx/v2/jwt"
+ "github.com/nuts-foundation/nuts-node/vcr/signature/proof"
"net/http"
"testing"
"time"
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rand"
+ "encoding/json"
+ "errors"
+ 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/auth/oauth"
"github.com/nuts-foundation/nuts-node/core"
"github.com/nuts-foundation/nuts-node/jsonld"
+ "github.com/nuts-foundation/nuts-node/vcr/credential"
+ "github.com/nuts-foundation/nuts-node/vcr/pe"
+ "github.com/nuts-foundation/nuts-node/vcr/test"
"github.com/nuts-foundation/nuts-node/vdr/resolver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+ "go.uber.org/mock/gomock"
)
func TestWrapper_RequestAccessToken(t *testing.T) {
@@ -105,16 +117,313 @@ func TestWrapper_RequestAccessToken(t *testing.T) {
})
}
+func TestWrapper_handleS2SAccessTokenRequest(t *testing.T) {
+ issuerDIDStr := "did:web:example.com:iam:123"
+ issuerDID := did.MustParseDID(issuerDIDStr)
+ const requestedScope = "eOverdracht-overdrachtsbericht"
+ // Create issuer DID document and keys
+ keyPair, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ issuerDIDDocument := did.Document{
+ ID: issuerDID,
+ }
+ keyID := did.DIDURL{DID: issuerDID}
+ keyID.Fragment = "1"
+ verificationMethod, err := did.NewVerificationMethod(keyID, ssi.JsonWebKey2020, issuerDID, keyPair.Public())
+ require.NoError(t, err)
+ issuerDIDDocument.AddAssertionMethod(verificationMethod)
+
+ var submission pe.PresentationSubmission
+ require.NoError(t, json.Unmarshal([]byte(`
+{
+ "descriptor_map": [
+ {
+ "id": "1",
+ "path": "$.verifiableCredential",
+ "format": "ldp_vc"
+ }
+ ]
+}`), &submission))
+ submissionJSONBytes, _ := json.Marshal(submission)
+ submissionJSON := string(submissionJSONBytes)
+ verifiableCredential := credential.ValidNutsOrganizationCredential(t)
+ subjectDID, _ := verifiableCredential.SubjectDID()
+ proofVisitor := test.LDProofVisitor(func(proof *proof.LDProof) {
+ proof.Domain = &issuerDIDStr
+ })
+ presentation := test.CreateJSONLDPresentation(t, *subjectDID, proofVisitor, verifiableCredential)
+
+ t.Run("JSON-LD VP", func(t *testing.T) {
+ ctx := newTestClient(t)
+ ctx.verifier.EXPECT().VerifyVP(presentation, true, true, gomock.Any()).Return(presentation.VerifiableCredential, nil)
+
+ resp, err := ctx.client.handleS2SAccessTokenRequest(issuerDID, requestedScope, submissionJSON, presentation.Raw())
+
+ require.NoError(t, err)
+ require.IsType(t, HandleTokenRequest200JSONResponse{}, resp)
+ tokenResponse := TokenResponse(resp.(HandleTokenRequest200JSONResponse))
+ assert.Equal(t, "bearer", tokenResponse.TokenType)
+ assert.Equal(t, requestedScope, *tokenResponse.Scope)
+ assert.Equal(t, int(accessTokenValidity.Seconds()), *tokenResponse.ExpiresIn)
+ assert.NotEmpty(t, tokenResponse.AccessToken)
+ })
+ t.Run("missing presentation expiry date", func(t *testing.T) {
+ ctx := newTestClient(t)
+ presentation := test.CreateJWTPresentation(t, *subjectDID, func(token jwt.Token) {
+ require.NoError(t, token.Remove(jwt.ExpirationKey))
+ }, verifiableCredential)
+
+ _, err := ctx.client.handleS2SAccessTokenRequest(issuerDID, requestedScope, submissionJSON, presentation.Raw())
+
+ require.EqualError(t, err, "invalid_request - presentation is missing creation or expiration date")
+ })
+ t.Run("missing presentation not before date", func(t *testing.T) {
+ ctx := newTestClient(t)
+ presentation := test.CreateJWTPresentation(t, *subjectDID, func(token jwt.Token) {
+ require.NoError(t, token.Remove(jwt.NotBeforeKey))
+ }, verifiableCredential)
+
+ _, err := ctx.client.handleS2SAccessTokenRequest(issuerDID, requestedScope, submissionJSON, presentation.Raw())
+
+ require.EqualError(t, err, "invalid_request - presentation is missing creation or expiration date")
+ })
+ t.Run("missing presentation valid for too long", func(t *testing.T) {
+ ctx := newTestClient(t)
+ presentation := test.CreateJWTPresentation(t, *subjectDID, func(token jwt.Token) {
+ require.NoError(t, token.Set(jwt.ExpirationKey, time.Now().Add(time.Hour)))
+ }, verifiableCredential)
+
+ _, err := ctx.client.handleS2SAccessTokenRequest(issuerDID, requestedScope, submissionJSON, presentation.Raw())
+
+ require.EqualError(t, err, "invalid_request - presentation is valid for too long (max 5s)")
+ })
+ t.Run("JWT VP", func(t *testing.T) {
+ ctx := newTestClient(t)
+ presentation := test.CreateJWTPresentation(t, *subjectDID, func(token jwt.Token) {
+ require.NoError(t, token.Set(jwt.AudienceKey, issuerDID.String()))
+ }, verifiableCredential)
+ ctx.verifier.EXPECT().VerifyVP(presentation, true, true, gomock.Any()).Return(presentation.VerifiableCredential, nil)
+
+ resp, err := ctx.client.handleS2SAccessTokenRequest(issuerDID, requestedScope, submissionJSON, presentation.Raw())
+
+ require.NoError(t, err)
+ require.IsType(t, HandleTokenRequest200JSONResponse{}, resp)
+ tokenResponse := TokenResponse(resp.(HandleTokenRequest200JSONResponse))
+ assert.Equal(t, "bearer", tokenResponse.TokenType)
+ assert.Equal(t, requestedScope, *tokenResponse.Scope)
+ assert.Equal(t, int(accessTokenValidity.Seconds()), *tokenResponse.ExpiresIn)
+ assert.NotEmpty(t, tokenResponse.AccessToken)
+ })
+ t.Run("VP is not valid JSON", func(t *testing.T) {
+ ctx := newTestClient(t)
+ resp, err := ctx.client.handleS2SAccessTokenRequest(issuerDID, requestedScope, submissionJSON, "[true, false]")
+
+ assert.EqualError(t, err, "invalid_request - assertion parameter is invalid: unable to parse PEX envelope as verifiable presentation: invalid JWT")
+ assert.Nil(t, resp)
+ })
+ t.Run("not all VPs have the same credential subject ID", func(t *testing.T) {
+ ctx := newTestClient(t)
+
+ secondSubjectID := did.MustParseDID("did:web:example.com:other")
+ secondPresentation := test.CreateJSONLDPresentation(t, secondSubjectID, proofVisitor, credential.JWTNutsOrganizationCredential(t, secondSubjectID))
+ assertionJSON, _ := json.Marshal([]VerifiablePresentation{presentation, secondPresentation})
+
+ resp, err := ctx.client.handleS2SAccessTokenRequest(issuerDID, requestedScope, submissionJSON, string(assertionJSON))
+ assert.EqualError(t, err, "invalid_request - not all presentations have the same credential subject ID")
+ assert.Nil(t, resp)
+ })
+ t.Run("nonce", func(t *testing.T) {
+ t.Run("replay attack (nonce is reused)", func(t *testing.T) {
+ ctx := newTestClient(t)
+ ctx.verifier.EXPECT().VerifyVP(presentation, true, true, gomock.Any()).Return(presentation.VerifiableCredential, nil)
+
+ _, err := ctx.client.handleS2SAccessTokenRequest(issuerDID, requestedScope, submissionJSON, presentation.Raw())
+ require.NoError(t, err)
+
+ resp, err := ctx.client.handleS2SAccessTokenRequest(issuerDID, requestedScope, submissionJSON, presentation.Raw())
+ assert.EqualError(t, err, "invalid_request - presentation nonce has already been used")
+ assert.Nil(t, resp)
+ })
+ t.Run("JSON-LD VP is missing nonce", func(t *testing.T) {
+ ctx := newTestClient(t)
+
+ proofVisitor := test.LDProofVisitor(func(proof *proof.LDProof) {
+ proof.Domain = &issuerDIDStr
+ proof.Nonce = nil
+ })
+ presentation := test.CreateJSONLDPresentation(t, *subjectDID, proofVisitor, verifiableCredential)
+
+ resp, err := ctx.client.handleS2SAccessTokenRequest(issuerDID, requestedScope, submissionJSON, presentation.Raw())
+ assert.EqualError(t, err, "invalid_request - presentation has invalid proof or nonce")
+ assert.Nil(t, resp)
+ })
+ t.Run("JSON-LD VP has empty nonce", func(t *testing.T) {
+ ctx := newTestClient(t)
+
+ proofVisitor := test.LDProofVisitor(func(proof *proof.LDProof) {
+ proof.Domain = &issuerDIDStr
+ proof.Nonce = new(string)
+ })
+ presentation := test.CreateJSONLDPresentation(t, *subjectDID, proofVisitor, verifiableCredential)
+
+ resp, err := ctx.client.handleS2SAccessTokenRequest(issuerDID, requestedScope, submissionJSON, presentation.Raw())
+ assert.EqualError(t, err, "invalid_request - presentation has invalid proof or nonce")
+ assert.Nil(t, resp)
+ })
+ t.Run("JWT VP is missing nonce", func(t *testing.T) {
+ ctx := newTestClient(t)
+ presentation := test.CreateJWTPresentation(t, *subjectDID, func(token jwt.Token) {
+ _ = token.Set(jwt.AudienceKey, issuerDID.String())
+ _ = token.Remove("nonce")
+ }, verifiableCredential)
+
+ _, err := ctx.client.handleS2SAccessTokenRequest(issuerDID, requestedScope, submissionJSON, presentation.Raw())
+
+ require.EqualError(t, err, "invalid_request - presentation has invalid/missing nonce")
+ })
+ t.Run("JWT VP has empty nonce", func(t *testing.T) {
+ ctx := newTestClient(t)
+ presentation := test.CreateJWTPresentation(t, *subjectDID, func(token jwt.Token) {
+ _ = token.Set(jwt.AudienceKey, issuerDID.String())
+ _ = token.Set("nonce", "")
+ }, verifiableCredential)
+
+ _, err := ctx.client.handleS2SAccessTokenRequest(issuerDID, requestedScope, submissionJSON, presentation.Raw())
+
+ require.EqualError(t, err, "invalid_request - presentation has invalid/missing nonce")
+ })
+ t.Run("JWT VP nonce is not a string", func(t *testing.T) {
+ ctx := newTestClient(t)
+ presentation := test.CreateJWTPresentation(t, *subjectDID, func(token jwt.Token) {
+ _ = token.Set(jwt.AudienceKey, issuerDID.String())
+ _ = token.Set("nonce", true)
+ }, verifiableCredential)
+
+ _, err := ctx.client.handleS2SAccessTokenRequest(issuerDID, requestedScope, submissionJSON, presentation.Raw())
+
+ require.EqualError(t, err, "invalid_request - presentation has invalid/missing nonce")
+ })
+ })
+ t.Run("audience", func(t *testing.T) {
+ t.Run("missing", func(t *testing.T) {
+ ctx := newTestClient(t)
+ presentation := test.CreateJWTPresentation(t, *subjectDID, nil, verifiableCredential)
+
+ resp, err := ctx.client.handleS2SAccessTokenRequest(issuerDID, requestedScope, submissionJSON, presentation.Raw())
+
+ assert.EqualError(t, err, "invalid_request - expected: did:web:example.com:iam:123, got: [] - presentation audience/domain is missing or does not match")
+ assert.Nil(t, resp)
+ })
+ t.Run("not matching", func(t *testing.T) {
+ ctx := newTestClient(t)
+ presentation := test.CreateJWTPresentation(t, *subjectDID, func(token jwt.Token) {
+ require.NoError(t, token.Set(jwt.AudienceKey, "did:example:other"))
+ }, verifiableCredential)
+
+ resp, err := ctx.client.handleS2SAccessTokenRequest(issuerDID, requestedScope, submissionJSON, presentation.Raw())
+
+ assert.EqualError(t, err, "invalid_request - expected: did:web:example.com:iam:123, got: [did:example:other] - presentation audience/domain is missing or does not match")
+ assert.Nil(t, resp)
+ })
+ })
+ t.Run("VP verification fails", func(t *testing.T) {
+ ctx := newTestClient(t)
+ ctx.verifier.EXPECT().VerifyVP(presentation, true, true, gomock.Any()).Return(nil, errors.New("invalid"))
+
+ resp, err := ctx.client.handleS2SAccessTokenRequest(issuerDID, requestedScope, submissionJSON, presentation.Raw())
+
+ assert.EqualError(t, err, "invalid_request - invalid - presentation(s) or contained credential(s) are invalid")
+ assert.Nil(t, resp)
+ })
+ t.Run("proof of ownership", func(t *testing.T) {
+ t.Run("VC without credentialSubject.id", func(t *testing.T) {
+ ctx := newTestClient(t)
+ presentation := test.CreateJSONLDPresentation(t, *subjectDID, proofVisitor, vc.VerifiableCredential{
+ CredentialSubject: []interface{}{map[string]string{}},
+ })
+
+ resp, err := ctx.client.handleS2SAccessTokenRequest(issuerDID, requestedScope, submissionJSON, presentation.Raw())
+
+ assert.EqualError(t, err, `invalid_request - unable to get subject DID from VC: credential subjects have no ID`)
+ assert.Nil(t, resp)
+ })
+ t.Run("signing key is not owned by credentialSubject.id", func(t *testing.T) {
+ ctx := newTestClient(t)
+ invalidProof := presentation.Proof[0].(map[string]interface{})
+ invalidProof["verificationMethod"] = "did:example:other#1"
+ verifiablePresentation := vc.VerifiablePresentation{
+ VerifiableCredential: []vc.VerifiableCredential{verifiableCredential},
+ Proof: []interface{}{invalidProof},
+ }
+ verifiablePresentationJSON, _ := verifiablePresentation.MarshalJSON()
+
+ resp, err := ctx.client.handleS2SAccessTokenRequest(issuerDID, requestedScope, submissionJSON, string(verifiablePresentationJSON))
+
+ assert.EqualError(t, err, `invalid_request - presentation signer is not credential subject`)
+ assert.Nil(t, resp)
+ })
+ })
+ t.Run("submission is not valid JSON", func(t *testing.T) {
+ ctx := newTestClient(t)
+
+ resp, err := ctx.client.handleS2SAccessTokenRequest(issuerDID, requestedScope, "not-a-valid-submission", presentation.Raw())
+
+ assert.EqualError(t, err, `invalid_request - invalid presentation submission: invalid character 'o' in literal null (expecting 'u')`)
+ assert.Nil(t, resp)
+ })
+ t.Run("unsupported scope", func(t *testing.T) {
+ ctx := newTestClient(t)
+
+ resp, err := ctx.client.handleS2SAccessTokenRequest(issuerDID, "everything", submissionJSON, presentation.Raw())
+
+ assert.EqualError(t, err, `invalid_scope - unsupported scope for presentation exchange: everything`)
+ assert.Nil(t, resp)
+ })
+ t.Run("re-evaluation of presentation definition yields different credentials", func(t *testing.T) {
+ // This indicates the client presented credentials that don't actually match the presentation definition,
+ // which could indicate a malicious client.
+ otherVerifiableCredential := vc.VerifiableCredential{
+ CredentialSubject: []interface{}{
+ map[string]interface{}{
+ "id": subjectDID.String(),
+ // just for demonstration purposes, what matters is that the credential does not match the presentation definition.
+ "IsAdministrator": true,
+ },
+ },
+ }
+ presentation := test.CreateJSONLDPresentation(t, *subjectDID, proofVisitor, otherVerifiableCredential)
+
+ ctx := newTestClient(t)
+
+ resp, err := ctx.client.handleS2SAccessTokenRequest(issuerDID, requestedScope, submissionJSON, presentation.Raw())
+ assert.EqualError(t, err, "invalid_request - presentation submission doesn't match presentation definition - presentation submission does not conform to Presentation Definition")
+ assert.Nil(t, resp)
+ })
+}
+
func TestWrapper_createAccessToken(t *testing.T) {
+ credentialSubjectID := did.MustParseDID("did:nuts:B8PUHs2AUHbFF1xLLK4eZjgErEcMXHxs68FteY7NDtCY")
+ verificationMethodID := ssi.MustParseURI(credentialSubjectID.String() + "#1")
credential, err := vc.ParseVerifiableCredential(jsonld.TestOrganizationCredential)
require.NoError(t, err)
- presentation := vc.VerifiablePresentation{
+ presentation := test.ParsePresentation(t, vc.VerifiablePresentation{
VerifiableCredential: []vc.VerifiableCredential{*credential},
+ Proof: []interface{}{
+ proof.LDProof{
+ VerificationMethod: verificationMethodID,
+ },
+ },
+ })
+ submission := pe.PresentationSubmission{
+ Id: "submissive",
+ }
+ definition := pe.PresentationDefinition{
+ Id: "definitive",
}
t.Run("ok", func(t *testing.T) {
ctx := newTestClient(t)
- accessToken, err := ctx.client.createAccessToken(issuerDID, time.Now(), presentation, "everything")
+ vps := []VerifiablePresentation{test.ParsePresentation(t, presentation)}
+ accessToken, err := ctx.client.createS2SAccessToken(issuerDID, time.Now(), vps, submission, definition, "everything", credentialSubjectID, nil)
require.NoError(t, err)
assert.NotEmpty(t, accessToken.AccessToken)
@@ -123,9 +432,11 @@ func TestWrapper_createAccessToken(t *testing.T) {
assert.Equal(t, "everything", *accessToken.Scope)
var storedToken AccessToken
- err = ctx.client.s2sAccessTokenStore().Get(accessToken.AccessToken, &storedToken)
+ err = ctx.client.accessTokenStore().Get(accessToken.AccessToken, &storedToken)
require.NoError(t, err)
assert.Equal(t, accessToken.AccessToken, storedToken.Token)
+ assert.Equal(t, submission, *storedToken.PresentationSubmission)
+ assert.Equal(t, definition, *storedToken.PresentationDefinition)
expectedVPJSON, _ := presentation.MarshalJSON()
actualVPJSON, _ := storedToken.VPToken[0].MarshalJSON()
assert.JSONEq(t, string(expectedVPJSON), string(actualVPJSON))
diff --git a/auth/api/iam/test/presentation_definition_mapping.json b/auth/api/iam/test/presentation_definition_mapping.json
index 319765aca3..33ebbf4377 100644
--- a/auth/api/iam/test/presentation_definition_mapping.json
+++ b/auth/api/iam/test/presentation_definition_mapping.json
@@ -1,6 +1,53 @@
{
"eOverdracht-overdrachtsbericht": {
"id": "eOverdracht",
- "input_descriptors": []
+ "input_descriptors": [
+ {
+ "id": "1",
+ "name": "Organization matcher",
+ "purpose": "Any care organization",
+ "constraints": {
+ "fields": [
+ {
+ "path": [
+ "$.credentialSubject.organization.city"
+ ],
+ "filter": {
+ "type": "string"
+ }
+ },
+ {
+ "path": [
+ "$.credentialSubject.organization.name"
+ ],
+ "filter": {
+ "type": "string"
+ }
+ },
+ {
+ "path": [
+ "$.type"
+ ],
+ "filter": {
+ "type": "string",
+ "const": "NutsOrganizationCredential"
+ }
+ }
+ ]
+ }
+ }
+ ],
+ "format": {
+ "jwt_vc": {
+ "alg": [
+ "ES256"
+ ]
+ },
+ "ldp_vc": {
+ "proof_type": [
+ "JsonWebSignature2020"
+ ]
+ }
+ }
}
}
diff --git a/auth/client/iam/client.go b/auth/client/iam/client.go
index d7cf10a4cb..508306205d 100644
--- a/auth/client/iam/client.go
+++ b/auth/client/iam/client.go
@@ -137,11 +137,12 @@ func (hb HTTPClient) AccessToken(ctx context.Context, tokenEndpoint string, vp v
}
// create a POST request with x-www-form-urlencoded body
- assertion, _ := json.Marshal(vp)
+ assertion := vp.Raw()
presentationSubmission, _ := json.Marshal(submission)
+ log.Logger().Tracef("Requesting access token from '%s' for scope '%s'\n VP: %s\n Submission: %s", presentationDefinitionURL.String(), scopes, assertion, string(presentationSubmission))
data := url.Values{}
data.Set(oauth.GrantTypeParam, oauth.VpTokenGrantType)
- data.Set(oauth.AssertionParam, string(assertion))
+ data.Set(oauth.AssertionParam, assertion)
data.Set(oauth.PresentationSubmissionParam, string(presentationSubmission))
data.Set(oauth.ScopeParam, scopes)
request, err := http.NewRequestWithContext(ctx, http.MethodPost, presentationDefinitionURL.String(), strings.NewReader(data.Encode()))
diff --git a/auth/oauth/openid.go b/auth/oauth/openid.go
index f657cc0f5a..852ecae90c 100644
--- a/auth/oauth/openid.go
+++ b/auth/oauth/openid.go
@@ -1,3 +1,21 @@
+/*
+ * 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 oauth
// algValuesSupported contains a list of supported cipher suites for jwt_vc_json & jwt_vp_json presentation formats
diff --git a/auth/services/oauth/relying_party.go b/auth/services/oauth/relying_party.go
index 1998e0fd97..308f5da302 100644
--- a/auth/services/oauth/relying_party.go
+++ b/auth/services/oauth/relying_party.go
@@ -179,15 +179,17 @@ func (s *relyingParty) RequestRFC021AccessToken(ctx context.Context, requester d
if signInstructions.Empty() {
return nil, core.Error(http.StatusPreconditionFailed, "no matching credentials")
}
- expires := time.Now().Add(time.Minute * 15) //todo
- nonce := nutsCrypto.GenerateNonce()
+ expires := time.Now().Add(time.Second * 5)
// todo: support multiple wallets
+ domain := verifier.String()
+ nonce := nutsCrypto.GenerateNonce()
vp, err := s.wallet.BuildPresentation(ctx, signInstructions[0].VerifiableCredentials, holder.PresentationOptions{
Format: format,
ProofOptions: proof.ProofOptions{
- Created: time.Now(),
- Challenge: &nonce,
- Expires: &expires,
+ Created: time.Now(),
+ Expires: &expires,
+ Domain: &domain,
+ Nonce: &nonce,
},
}, &requester, false)
if err != nil {
diff --git a/docs/_static/auth/iam.yaml b/docs/_static/auth/iam.yaml
index d25d49324e..e458cc3619 100644
--- a/docs/_static/auth/iam.yaml
+++ b/docs/_static/auth/iam.yaml
@@ -52,16 +52,18 @@ paths:
type: object
required:
- grant_type
- - code
properties:
grant_type:
type: string
example: urn:ietf:params:oauth:grant-type:authorized_code
code:
type: string
- example: secret
- additionalProperties:
- type: string
+ assertion:
+ type: string
+ presentation_submission:
+ type: string
+ scope:
+ type: string
responses:
"200":
description: OK
diff --git a/docs/pages/deployment/cli-reference.rst b/docs/pages/deployment/cli-reference.rst
index b32c1400b4..dc1fe5f6f5 100755
--- a/docs/pages/deployment/cli-reference.rst
+++ b/docs/pages/deployment/cli-reference.rst
@@ -412,7 +412,7 @@ Print conflicted documents and their metadata
nuts vdr create-did
^^^^^^^^^^^^^^^^^^^
-When using the V2 API, a web:did will be created. All the other options are ignored for a web:did.
+When using the V2 API, a did:web DID will be created. All the other options are ignored for did:web.
::
@@ -430,7 +430,7 @@ When using the V2 API, a web:did will be created. All the other options are igno
--timeout duration Client time-out when performing remote operations, such as '500ms' or '10s'. Refer to Golang's 'time.Duration' syntax for a more elaborate description of the syntax. (default 10s)
--token string Token to be used for authenticating on the remote node. Takes precedence over 'token-file'.
--token-file string File from which the authentication token will be read. If not specified it will try to read the token from the '.nuts-client.cfg' file in the user's home dir.
- --v2 Pass 'true' to use the V2 API and create a web:did.
+ --v2 Pass 'true' to use the V2 API and create a did:web DID.
--verbosity string Log level (trace, debug, info, warn, error) (default "info")
nuts vdr deactivate
diff --git a/e2e-tests/oauth-flow/rfc021/docker-compose.yml b/e2e-tests/oauth-flow/rfc021/docker-compose.yml
index 57dae33a7d..c34f0bad59 100644
--- a/e2e-tests/oauth-flow/rfc021/docker-compose.yml
+++ b/e2e-tests/oauth-flow/rfc021/docker-compose.yml
@@ -11,6 +11,9 @@ services:
- "./node-A/data:/opt/nuts/data:rw"
- "../../tls-certs/nodeA-backend-certificate.pem:/opt/nuts/certificate-and-key.pem:ro"
- "../../tls-certs/truststore.pem:/opt/nuts/truststore.pem:ro"
+ # did:web resolver uses the OS CA bundle, but e2e tests use a self-signed CA which can be found in truststore.pem
+ # So we need to mount that file to the OS CA bundle location, otherwise did:web resolving will fail due to untrusted certs.
+ - "../../tls-certs/truststore.pem:/etc/ssl/certs/Nuts_RootCA.pem:ro"
- "./node-A/presentationexchangemapping.json:/opt/nuts/presentationexchangemapping.json:ro"
healthcheck:
interval: 1s # Make test run quicker by checking health status more often
@@ -36,6 +39,9 @@ services:
- "../../tls-certs/nodeB-certificate.pem:/opt/nuts/certificate-and-key.pem:ro"
- "../../tls-certs/truststore.pem:/opt/nuts/truststore.pem:ro"
- "../../tls-certs/truststore.pem:/etc/ssl/certs/truststore.pem:ro"
+ # did:web resolver uses the OS CA bundle, but e2e tests use a self-signed CA which can be found in truststore.pem
+ # So we need to mount that file to the OS CA bundle location, otherwise did:web resolving will fail due to untrusted certs.
+ - "../../tls-certs/truststore.pem:/etc/ssl/certs/Nuts_RootCA.pem:ro"
healthcheck:
interval: 1s # Make test run quicker by checking health status more often
nodeB:
diff --git a/e2e-tests/oauth-flow/rfc021/run-test.sh b/e2e-tests/oauth-flow/rfc021/run-test.sh
index 6e68322cd3..7fafab3399 100755
--- a/e2e-tests/oauth-flow/rfc021/run-test.sh
+++ b/e2e-tests/oauth-flow/rfc021/run-test.sh
@@ -52,30 +52,21 @@ echo "---------------------------------------"
echo "Perform OAuth 2.0 rfc021 flow..."
echo "---------------------------------------"
# Request access token
-# Create DID for A with :nuts: replaced with :web:
REQUEST="{\"verifier\":\"${VENDOR_A_DID}\",\"scope\":\"test\"}"
RESPONSE=$(echo $REQUEST | curl -X POST -s --data-binary @- http://localhost:21323/internal/auth/v2/$VENDOR_B_DID/request-access-token -H "Content-Type:application/json" -v)
-#if echo $RESPONSE | grep -q "access_token"; then
-# echo $RESPONSE | sed -E 's/.*"access_token":"([^"]*).*/\1/' > ./node-B/data/accesstoken.txt
-# echo "access token stored in ./node-B/data/accesstoken.txt"
-#else
-# echo "FAILED: Could not get access token from node-A" 1>&2
-# echo $RESPONSE
-# exitWithDockerLogs 1
-#fi
-if echo $RESPONSE | grep -q "unsupported_grant_type - not implemented yet"; then
- echo "Good so far!"
+if echo $RESPONSE | grep -q "access_token"; then
+ echo $RESPONSE | sed -E 's/.*"access_token":"([^"]*).*/\1/' > ./node-B/data/accesstoken.txt
+ echo "access token stored in ./node-B/data/accesstoken.txt"
else
echo "FAILED: Could not get access token from node-A" 1>&2
echo $RESPONSE
exitWithDockerLogs 1
fi
-#echo "------------------------------------"
-#echo "Retrieving data..."
-#echo "------------------------------------"
-#
-#RESPONSE=$(docker compose exec nodeB curl --insecure --cert /opt/nuts/certificate-and-key.pem --key /opt/nuts/certificate-and-key.pem https://nodeA:443/ping -H "Authorization: bearer $(cat ./node-B/data/accesstoken.txt)" -v)
+echo "------------------------------------"
+echo "Retrieving data..."
+echo "------------------------------------"
+#RESPONSE=$(docker compose exec nodeB curl --insecure --cert /etc/nginx/ssl/server.pem --key /etc/nginx/ssl/key.pem https://nodeA:443/ping -H "Authorization: bearer $(cat ./node-B/data/accesstoken.txt)" -v)
#if echo $RESPONSE | grep -q "pong"; then
# echo "success!"
#else
diff --git a/vcr/assets/test_assets.go b/vcr/assets/test_assets.go
index 355920dc2d..bdf0e1e490 100644
--- a/vcr/assets/test_assets.go
+++ b/vcr/assets/test_assets.go
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 Nuts community
+ * 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
@@ -18,7 +18,9 @@
package assets
-import "embed"
+import (
+ "embed"
+)
// TestAssets contains the embedded test files needed for VCR.
//
diff --git a/vcr/credential/resolver.go b/vcr/credential/resolver.go
index eda669ff84..b6644fc370 100644
--- a/vcr/credential/resolver.go
+++ b/vcr/credential/resolver.go
@@ -70,20 +70,30 @@ func PresentationSigner(presentation vc.VerifiablePresentation) (*did.DID, error
return nil, errors.New("JWT presentation does not have 'iss' claim")
}
return did.ParseDID(issuer)
- case vc.JSONLDCredentialProofFormat:
- fallthrough
- default:
- var proofs []proof.LDProof
- if err := presentation.UnmarshalProofValue(&proofs); err != nil {
- return nil, fmt.Errorf("invalid LD-proof for presentation: %w", err)
- }
- if len(proofs) != 1 {
- return nil, fmt.Errorf("presentation should have exactly 1 proof, got %d", len(proofs))
+ case vc.JSONLDPresentationProofFormat:
+ proof, err := ParseLDProof(presentation)
+ if err != nil {
+ return nil, err
}
- verificationMethod, err := did.ParseDIDURL(proofs[0].VerificationMethod.String())
+ verificationMethod, err := did.ParseDIDURL(proof.VerificationMethod.String())
if err != nil || verificationMethod.DID.Empty() {
return nil, fmt.Errorf("invalid verification method for JSON-LD presentation: %w", err)
}
return &verificationMethod.DID, nil
+ default:
+ return nil, fmt.Errorf("unsupported presentation format: %s", presentation.Format())
+ }
+}
+
+// ParseLDProof parses the LinkedData proof from the presentation.
+// It returns an error if the presentation does not have exactly 1 proof.
+func ParseLDProof(presentation vc.VerifiablePresentation) (*proof.LDProof, error) {
+ var proofs []proof.LDProof
+ if err := presentation.UnmarshalProofValue(&proofs); err != nil {
+ return nil, fmt.Errorf("invalid LD-proof for presentation: %w", err)
+ }
+ if len(proofs) != 1 {
+ return nil, fmt.Errorf("presentation should have exactly 1 proof, got %d", len(proofs))
}
+ return &proofs[0], nil
}
diff --git a/vcr/credential/resolver_test.go b/vcr/credential/resolver_test.go
index 310e364659..2983f0e347 100644
--- a/vcr/credential/resolver_test.go
+++ b/vcr/credential/resolver_test.go
@@ -27,6 +27,7 @@ import (
"github.com/lestrrat-go/jwx/v2/jwt"
"github.com/nuts-foundation/go-did/did"
"github.com/nuts-foundation/nuts-node/vcr/signature/proof"
+ "github.com/nuts-foundation/nuts-node/vcr/test"
"github.com/stretchr/testify/require"
"testing"
@@ -98,54 +99,60 @@ func TestPresentationSigner(t *testing.T) {
})
t.Run("JSON-LD", func(t *testing.T) {
t.Run("ok", func(t *testing.T) {
- presentation := vc.VerifiablePresentation{
+ presentation := test.ParsePresentation(t, vc.VerifiablePresentation{
Proof: []interface{}{proof.LDProof{
VerificationMethod: keyID.URI(),
}},
- }
+ })
actual, err := PresentationSigner(presentation)
- assert.NoError(t, err)
+ require.NoError(t, err)
assert.Equal(t, keyID.DID, *actual)
})
t.Run("too many proofs", func(t *testing.T) {
- presentation := vc.VerifiablePresentation{
+ presentation := test.ParsePresentation(t, vc.VerifiablePresentation{
Proof: []interface{}{proof.LDProof{
VerificationMethod: keyID.URI(),
}, proof.LDProof{
VerificationMethod: keyID.URI(),
}},
- }
+ })
actual, err := PresentationSigner(presentation)
assert.EqualError(t, err, "presentation should have exactly 1 proof, got 2")
assert.Nil(t, actual)
})
t.Run("not a JSON-LD proof", func(t *testing.T) {
- presentation := vc.VerifiablePresentation{
+ presentation := test.ParsePresentation(t, vc.VerifiablePresentation{
Proof: []interface{}{5},
- }
+ })
actual, err := PresentationSigner(presentation)
assert.EqualError(t, err, "invalid LD-proof for presentation: json: cannot unmarshal number into Go value of type proof.LDProof")
assert.Nil(t, actual)
})
t.Run("invalid DID in proof", func(t *testing.T) {
- presentation := vc.VerifiablePresentation{
+ presentation := test.ParsePresentation(t, vc.VerifiablePresentation{
Proof: []interface{}{proof.LDProof{
VerificationMethod: ssi.MustParseURI("foo"),
}},
- }
+ })
actual, err := PresentationSigner(presentation)
assert.EqualError(t, err, "invalid verification method for JSON-LD presentation: invalid DID")
assert.Nil(t, actual)
})
t.Run("empty VerificationMethod", func(t *testing.T) {
- presentation := vc.VerifiablePresentation{
+ presentation := test.ParsePresentation(t, vc.VerifiablePresentation{
Proof: []interface{}{proof.LDProof{
VerificationMethod: ssi.MustParseURI(""),
}},
- }
+ })
actual, err := PresentationSigner(presentation)
assert.ErrorContains(t, err, "invalid verification method for JSON-LD presentation")
assert.Nil(t, actual)
})
})
+ t.Run("unsupported format", func(t *testing.T) {
+ presentation := vc.VerifiablePresentation{}
+ actual, err := PresentationSigner(presentation)
+ assert.EqualError(t, err, "unsupported presentation format: ")
+ assert.Nil(t, actual)
+ })
}
diff --git a/vcr/credential/test.go b/vcr/credential/test.go
index 4e1f7d87d5..33de4bcd0e 100644
--- a/vcr/credential/test.go
+++ b/vcr/credential/test.go
@@ -26,6 +26,7 @@ import (
"encoding/json"
"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jwt"
+ "github.com/nuts-foundation/go-did/did"
"github.com/nuts-foundation/nuts-node/vcr/assets"
"github.com/stretchr/testify/require"
"testing"
@@ -75,12 +76,13 @@ func ValidNutsOrganizationCredential(t *testing.T) vc.VerifiableCredential {
return inputVC
}
-func JWTNutsOrganizationCredential(t *testing.T) vc.VerifiableCredential {
+func JWTNutsOrganizationCredential(t *testing.T, subjectID did.DID) vc.VerifiableCredential {
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
token := jwt.New()
require.NoError(t, token.Set("vc", map[string]interface{}{
"credentialSubject": map[string]interface{}{
+ "id": subjectID,
"organization": map[string]interface{}{
"city": "IJbergen",
"name": "care",
diff --git a/vcr/credential/util.go b/vcr/credential/util.go
new file mode 100644
index 0000000000..afbf2d1884
--- /dev/null
+++ b/vcr/credential/util.go
@@ -0,0 +1,110 @@
+/*
+ * 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 credential
+
+import (
+ "errors"
+ "github.com/nuts-foundation/go-did/did"
+ "github.com/nuts-foundation/go-did/vc"
+ "time"
+)
+
+// ResolveSubjectDID resolves the subject DID from the given credentials.
+// It returns an error if:
+// - the credentials do not have the same subject DID.
+// - the credentials do not have a subject DID.
+func ResolveSubjectDID(credentials ...vc.VerifiableCredential) (*did.DID, error) {
+ var subjectID did.DID
+ for _, credential := range credentials {
+ sid, err := credential.SubjectDID()
+ if err != nil {
+ return nil, err
+ }
+ if !subjectID.Empty() && !subjectID.Equals(*sid) {
+ return nil, errors.New("not all VCs have the same credentialSubject.id")
+ }
+ subjectID = *sid
+ }
+ return &subjectID, nil
+}
+
+// PresenterIsCredentialSubject checks if the presenter of the VP is the same as the subject of the VCs being presented.
+// If the presentation signer or credential subject can't be resolved, it returns an error.
+// If parsing succeeds and the signer DID is the same as the credential subject DID, it returns the DID.
+func PresenterIsCredentialSubject(vp vc.VerifiablePresentation) (*did.DID, error) {
+ signerDID, err := PresentationSigner(vp)
+ if err != nil {
+ return nil, err
+ }
+ credentialSubjectID, err := ResolveSubjectDID(vp.VerifiableCredential...)
+ if err != nil {
+ return nil, err
+ }
+ if !credentialSubjectID.Equals(*signerDID) {
+ return nil, nil
+ }
+ return signerDID, nil
+}
+
+// PresentationIssuanceDate returns the date at which the presentation was issued.
+// For JSON-LD, it looks at the first LinkedData proof's 'created' property.
+// For JWT, it looks at the 'nbf' claim, or if that is not present, the 'iat' claim.
+// If it can't resolve the date, it returns nil.
+func PresentationIssuanceDate(presentation vc.VerifiablePresentation) *time.Time {
+ var result time.Time
+ switch presentation.Format() {
+ case vc.JWTPresentationProofFormat:
+ jwt := presentation.JWT()
+ if result = jwt.NotBefore(); result.IsZero() {
+ result = jwt.IssuedAt()
+ }
+ case vc.JSONLDPresentationProofFormat:
+ ldProof, err := ParseLDProof(presentation)
+ if err != nil {
+ return nil
+ }
+ result = ldProof.Created
+ }
+ if result.IsZero() {
+ return nil
+ }
+ return &result
+}
+
+// PresentationExpirationDate returns the date at which the presentation was issued.
+// For JSON-LD, it looks at the first LinkedData proof's 'expires' property.
+// For JWT, it looks at the 'exp' claim.
+// If it can't resolve the date, it returns nil.
+func PresentationExpirationDate(presentation vc.VerifiablePresentation) *time.Time {
+ var result time.Time
+ switch presentation.Format() {
+ case vc.JWTPresentationProofFormat:
+ result = presentation.JWT().Expiration()
+ case vc.JSONLDPresentationProofFormat:
+ ldProof, err := ParseLDProof(presentation)
+ if err != nil || ldProof.Expires == nil {
+ return nil
+ }
+ result = *ldProof.Expires
+ }
+ if result.IsZero() {
+ return nil
+ }
+ return &result
+}
diff --git a/vcr/credential/util_test.go b/vcr/credential/util_test.go
new file mode 100644
index 0000000000..e7eba1c518
--- /dev/null
+++ b/vcr/credential/util_test.go
@@ -0,0 +1,289 @@
+/*
+ * 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 credential
+
+import (
+ "github.com/lestrrat-go/jwx/v2/jwt"
+ ssi "github.com/nuts-foundation/go-did"
+ "github.com/nuts-foundation/go-did/did"
+ "github.com/nuts-foundation/go-did/vc"
+ "github.com/nuts-foundation/nuts-node/vcr/signature/proof"
+ "github.com/nuts-foundation/nuts-node/vcr/test"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "testing"
+ "time"
+)
+
+func TestResolveSubjectDID(t *testing.T) {
+ did1 := did.MustParseDID("did:test:123")
+ did2 := did.MustParseDID("did:test:456")
+ credential1 := vc.VerifiableCredential{
+ CredentialSubject: []interface{}{map[string]interface{}{"id": did1}},
+ }
+ credential2 := vc.VerifiableCredential{
+ CredentialSubject: []interface{}{map[string]interface{}{"id": did1}},
+ }
+ credential3 := vc.VerifiableCredential{
+ CredentialSubject: []interface{}{map[string]interface{}{"id": did2}},
+ }
+ t.Run("all the same", func(t *testing.T) {
+ actual, err := ResolveSubjectDID(credential1, credential2)
+ assert.NoError(t, err)
+ assert.Equal(t, did1, *actual)
+ })
+ t.Run("differ", func(t *testing.T) {
+ actual, err := ResolveSubjectDID(credential1, credential3)
+ assert.EqualError(t, err, "not all VCs have the same credentialSubject.id")
+ assert.Nil(t, actual)
+ })
+ t.Run("no ID", func(t *testing.T) {
+ actual, err := ResolveSubjectDID(vc.VerifiableCredential{CredentialSubject: []interface{}{map[string]interface{}{}}})
+ assert.EqualError(t, err, "unable to get subject DID from VC: credential subjects have no ID")
+ assert.Nil(t, actual)
+ })
+ t.Run("no credentialSubject", func(t *testing.T) {
+ actual, err := ResolveSubjectDID(vc.VerifiableCredential{})
+ assert.EqualError(t, err, "unable to get subject DID from VC: there must be at least 1 credentialSubject")
+ assert.Nil(t, actual)
+ })
+
+}
+
+func TestPresenterIsCredentialSubject(t *testing.T) {
+ subjectDID := did.MustParseDID("did:test:123")
+ keyID := ssi.MustParseURI("did:test:123#1")
+ t.Run("ok", func(t *testing.T) {
+ vp := test.ParsePresentation(t, vc.VerifiablePresentation{
+ Proof: []interface{}{
+ proof.LDProof{
+ Type: "JsonWebSignature2020",
+ VerificationMethod: keyID,
+ },
+ },
+ VerifiableCredential: []vc.VerifiableCredential{
+ {
+ CredentialSubject: []interface{}{map[string]interface{}{"id": subjectDID}},
+ },
+ },
+ })
+ is, err := PresenterIsCredentialSubject(vp)
+ assert.NoError(t, err)
+ assert.Equal(t, subjectDID, *is)
+ })
+ t.Run("no proof", func(t *testing.T) {
+ vp := test.ParsePresentation(t, vc.VerifiablePresentation{})
+ actual, err := PresenterIsCredentialSubject(vp)
+ assert.EqualError(t, err, "presentation should have exactly 1 proof, got 0")
+ assert.Nil(t, actual)
+ })
+ t.Run("no VC subject", func(t *testing.T) {
+ vp := test.ParsePresentation(t, vc.VerifiablePresentation{
+ Proof: []interface{}{
+ proof.LDProof{
+ Type: "JsonWebSignature2020",
+ VerificationMethod: keyID,
+ },
+ },
+ VerifiableCredential: []vc.VerifiableCredential{
+ {},
+ },
+ })
+ is, err := PresenterIsCredentialSubject(vp)
+ assert.EqualError(t, err, "unable to get subject DID from VC: there must be at least 1 credentialSubject")
+ assert.Nil(t, is)
+ })
+ t.Run("no VC subject ID", func(t *testing.T) {
+ vp := test.ParsePresentation(t, vc.VerifiablePresentation{
+ Proof: []interface{}{
+ proof.LDProof{
+ Type: "JsonWebSignature2020",
+ VerificationMethod: keyID,
+ },
+ },
+ VerifiableCredential: []vc.VerifiableCredential{
+ {
+ CredentialSubject: []interface{}{map[string]interface{}{}},
+ },
+ },
+ })
+ is, err := PresenterIsCredentialSubject(vp)
+ assert.EqualError(t, err, "unable to get subject DID from VC: credential subjects have no ID")
+ assert.Nil(t, is)
+ })
+ t.Run("proof verification method does not equal VC subject ID", func(t *testing.T) {
+ vp := test.ParsePresentation(t, vc.VerifiablePresentation{
+ Proof: []interface{}{
+ proof.LDProof{
+ Type: "JsonWebSignature2020",
+ VerificationMethod: keyID,
+ },
+ },
+ VerifiableCredential: []vc.VerifiableCredential{
+ {
+ CredentialSubject: []interface{}{map[string]interface{}{"id": did.MustParseDID("did:test:456")}},
+ },
+ },
+ })
+ is, err := PresenterIsCredentialSubject(vp)
+ assert.NoError(t, err)
+ assert.Nil(t, is)
+ })
+ t.Run("proof type is unsupported", func(t *testing.T) {
+ vp := test.ParsePresentation(t, vc.VerifiablePresentation{
+ Proof: []interface{}{
+ true,
+ },
+ VerifiableCredential: []vc.VerifiableCredential{
+ {
+ CredentialSubject: []interface{}{map[string]interface{}{"id": subjectDID}},
+ },
+ },
+ })
+ is, err := PresenterIsCredentialSubject(vp)
+ assert.EqualError(t, err, "invalid LD-proof for presentation: json: cannot unmarshal bool into Go value of type proof.LDProof")
+ assert.Nil(t, is)
+ })
+ t.Run("too many proofs", func(t *testing.T) {
+ vp := test.ParsePresentation(t, vc.VerifiablePresentation{
+ Proof: []interface{}{
+ proof.LDProof{},
+ proof.LDProof{},
+ },
+ VerifiableCredential: []vc.VerifiableCredential{
+ {
+ CredentialSubject: []interface{}{map[string]interface{}{"id": subjectDID}},
+ },
+ },
+ })
+ is, err := PresenterIsCredentialSubject(vp)
+ assert.EqualError(t, err, "presentation should have exactly 1 proof, got 2")
+ assert.Nil(t, is)
+ })
+}
+
+func TestPresentationIssuanceDate(t *testing.T) {
+ presenterDID := did.MustParseDID("did:test:123")
+ expected := time.Now().In(time.UTC).Truncate(time.Second)
+ t.Run("JWT iat", func(t *testing.T) {
+ presentation := test.CreateJWTPresentation(t, presenterDID, func(token jwt.Token) {
+ _ = token.Remove(jwt.NotBeforeKey)
+ require.NoError(t, token.Set(jwt.IssuedAtKey, expected))
+ })
+ actual := PresentationIssuanceDate(presentation)
+ assert.Equal(t, expected, *actual)
+ })
+ t.Run("JWT nbf", func(t *testing.T) {
+ presentation := test.CreateJWTPresentation(t, presenterDID, func(token jwt.Token) {
+ _ = token.Remove(jwt.IssuedAtKey)
+ require.NoError(t, token.Set(jwt.NotBeforeKey, expected))
+ })
+ actual := PresentationIssuanceDate(presentation)
+ assert.Equal(t, expected, *actual)
+ })
+ t.Run("JWT nbf takes precedence over iat", func(t *testing.T) {
+ presentation := test.CreateJWTPresentation(t, presenterDID, func(token jwt.Token) {
+ require.NoError(t, token.Set(jwt.IssuedAtKey, expected.Add(time.Hour)))
+ require.NoError(t, token.Set(jwt.NotBeforeKey, expected))
+ })
+ actual := PresentationIssuanceDate(presentation)
+ assert.Equal(t, expected, *actual)
+ })
+ t.Run("JWT no iat or nbf", func(t *testing.T) {
+ presentation := test.CreateJWTPresentation(t, presenterDID, func(token jwt.Token) {
+ _ = token.Remove(jwt.IssuedAtKey)
+ _ = token.Remove(jwt.NotBeforeKey)
+ })
+ actual := PresentationIssuanceDate(presentation)
+ assert.Nil(t, actual)
+ })
+ t.Run("JSON-LD", func(t *testing.T) {
+ presentation := test.ParsePresentation(t, vc.VerifiablePresentation{
+ Proof: []interface{}{
+ proof.LDProof{
+ ProofOptions: proof.ProofOptions{
+ Created: expected,
+ },
+ },
+ },
+ })
+ actual := PresentationIssuanceDate(presentation)
+ assert.Equal(t, expected, *actual)
+ })
+ t.Run("JSON-LD no proof", func(t *testing.T) {
+ presentation := test.ParsePresentation(t, vc.VerifiablePresentation{})
+ actual := PresentationIssuanceDate(presentation)
+ assert.Nil(t, actual)
+ })
+ t.Run("JSON-LD no created", func(t *testing.T) {
+ presentation := test.ParsePresentation(t, vc.VerifiablePresentation{
+ Proof: []interface{}{
+ proof.LDProof{},
+ },
+ })
+ actual := PresentationIssuanceDate(presentation)
+ assert.Nil(t, actual)
+ })
+}
+
+func TestPresentationExpirationDate(t *testing.T) {
+ presenterDID := did.MustParseDID("did:test:123")
+ expected := time.Now().In(time.UTC).Truncate(time.Second)
+ t.Run("JWT", func(t *testing.T) {
+ presentation := test.CreateJWTPresentation(t, presenterDID, func(token jwt.Token) {
+ require.NoError(t, token.Set(jwt.ExpirationKey, expected))
+ })
+ actual := PresentationExpirationDate(presentation)
+ assert.Equal(t, expected, *actual)
+ })
+ t.Run("JWT no exp", func(t *testing.T) {
+ presentation := test.CreateJWTPresentation(t, presenterDID, func(token jwt.Token) {
+ _ = token.Remove(jwt.ExpirationKey)
+ })
+ actual := PresentationExpirationDate(presentation)
+ assert.Nil(t, actual)
+ })
+ t.Run("JSON-LD", func(t *testing.T) {
+ presentation := test.ParsePresentation(t, vc.VerifiablePresentation{
+ Proof: []interface{}{
+ proof.LDProof{
+ ProofOptions: proof.ProofOptions{
+ Expires: &expected,
+ },
+ },
+ },
+ })
+ actual := PresentationExpirationDate(presentation)
+ assert.Equal(t, expected, *actual)
+ })
+ t.Run("JSON-LD no proof", func(t *testing.T) {
+ presentation := test.ParsePresentation(t, vc.VerifiablePresentation{})
+ actual := PresentationExpirationDate(presentation)
+ assert.Nil(t, actual)
+ })
+ t.Run("JSON-LD no expires", func(t *testing.T) {
+ presentation := test.ParsePresentation(t, vc.VerifiablePresentation{
+ Proof: []interface{}{
+ proof.LDProof{},
+ },
+ })
+ actual := PresentationExpirationDate(presentation)
+ assert.Nil(t, actual)
+ })
+}
diff --git a/vcr/holder/wallet.go b/vcr/holder/wallet.go
index ca5d6275dc..38b8761a4e 100644
--- a/vcr/holder/wallet.go
+++ b/vcr/holder/wallet.go
@@ -34,6 +34,7 @@ import (
"github.com/nuts-foundation/nuts-node/core"
"github.com/nuts-foundation/nuts-node/crypto"
"github.com/nuts-foundation/nuts-node/jsonld"
+ "github.com/nuts-foundation/nuts-node/vcr/credential"
"github.com/nuts-foundation/nuts-node/vcr/log"
"github.com/nuts-foundation/nuts-node/vcr/signature"
"github.com/nuts-foundation/nuts-node/vcr/signature/proof"
@@ -71,7 +72,7 @@ func New(
func (h wallet) BuildPresentation(ctx context.Context, credentials []vc.VerifiableCredential, options PresentationOptions, signerDID *did.DID, validateVC bool) (*vc.VerifiablePresentation, error) {
var err error
if signerDID == nil {
- signerDID, err = h.resolveSubjectDID(credentials)
+ signerDID, err = credential.ResolveSubjectDID(credentials...)
if err != nil {
return nil, fmt.Errorf("unable to resolve signer DID from VCs for creating VP: %w", err)
}
@@ -124,6 +125,12 @@ func (h wallet) buildJWTPresentation(ctx context.Context, subjectDID did.DID, cr
VerifiableCredential: credentials,
},
}
+ if options.ProofOptions.Nonce != nil {
+ claims["nonce"] = *options.ProofOptions.Nonce
+ }
+ if options.ProofOptions.Domain != nil {
+ claims[jwt.AudienceKey] = *options.ProofOptions.Domain
+ }
if options.ProofOptions.Created.IsZero() {
claims[jwt.NotBeforeKey] = time.Now().Unix()
} else {
@@ -166,9 +173,8 @@ func (h wallet) buildJSONLDPresentation(ctx context.Context, subjectDID did.DID,
return nil, err
}
- // TODO: choose between different proof types (JWT or LD-Proof)
- signingResult, err := proof.
- NewLDProof(options.ProofOptions).
+ ldProof := proof.NewLDProof(options.ProofOptions)
+ signingResult, err := ldProof.
Sign(ctx, document, signature.JSONWebSignature2020{ContextLoader: h.jsonldManager.DocumentLoader(), Signer: h.keyStore}, key)
if err != nil {
return nil, fmt.Errorf("unable to sign VP with LD proof: %w", err)
@@ -181,12 +187,12 @@ func (h wallet) Put(ctx context.Context, credentials ...vc.VerifiableCredential)
err := h.walletStore.Write(ctx, func(tx stoabs.WriteTx) error {
stats := tx.GetShelfWriter(statsShelf)
var newCredentials uint32
- for _, credential := range credentials {
- subjectDID, err := h.resolveSubjectDID([]vc.VerifiableCredential{credential})
+ for _, curr := range credentials {
+ subjectDID, err := curr.SubjectDID()
if err != nil {
- return fmt.Errorf("unable to resolve subject DID from VC %s: %w", credential.ID, err)
+ return fmt.Errorf("unable to resolve subject DID from VC %s: %w", curr.ID, err)
}
- walletKey := stoabs.BytesKey(credential.ID.String())
+ walletKey := stoabs.BytesKey(curr.ID.String())
// First check if the VC doesn't already exist; otherwise stats will be incorrect
walletShelf := tx.GetShelfWriter(subjectDID.String())
_, err = walletShelf.Get(walletKey)
@@ -195,13 +201,13 @@ func (h wallet) Put(ctx context.Context, credentials ...vc.VerifiableCredential)
continue
} else if !errors.Is(err, stoabs.ErrKeyNotFound) {
// Other error
- return fmt.Errorf("unable to check if credential %s already exists: %w", credential.ID, err)
+ return fmt.Errorf("unable to check if credential %s already exists: %w", curr.ID, err)
}
// Write credential
- data, _ := credential.MarshalJSON()
+ data, _ := curr.MarshalJSON()
err = walletShelf.Put(walletKey, data)
if err != nil {
- return fmt.Errorf("unable to store credential %s: %w", credential.ID, err)
+ return fmt.Errorf("unable to store credential %s: %w", curr.ID, err)
}
newCredentials++
}
@@ -267,26 +273,6 @@ func (h wallet) IsEmpty() (bool, error) {
return count == 0, err
}
-func (h wallet) resolveSubjectDID(credentials []vc.VerifiableCredential) (*did.DID, error) {
- var subjectID did.DID
- for _, credential := range credentials {
- sid, err := credential.SubjectDID()
- if err != nil {
- return nil, err
- }
- if !subjectID.Empty() && !subjectID.Equals(*sid) {
- return nil, errors.New("not all VCs have the same credentialSubject.id")
- }
- subjectID = *sid
- }
-
- if subjectID.Empty() {
- return nil, errors.New("could not resolve subject DID from VCs")
- }
-
- return &subjectID, nil
-}
-
func (h wallet) readCredentialCount(statsShelf stoabs.Reader) (uint32, error) {
countBytes, err := statsShelf.Get(credentialCountStatsKey)
if errors.Is(err, stoabs.ErrKeyNotFound) {
diff --git a/vcr/holder/wallet_test.go b/vcr/holder/wallet_test.go
index 6d6d0b0df8..10c0521370 100644
--- a/vcr/holder/wallet_test.go
+++ b/vcr/holder/wallet_test.go
@@ -89,15 +89,22 @@ func TestWallet_BuildPresentation(t *testing.T) {
assert.Equal(t, testDID, did.MustParseDIDURL(result.ID.String()).DID, "id must be the DID of the holder")
assert.NotEmpty(t, result.ID.Fragment, "id must have a fragment")
assert.Equal(t, JSONLDPresentationFormat, result.Format())
+ ldProof, err := credential.ParseLDProof(*result)
+ require.NoError(t, err)
+ assert.Empty(t, ldProof.Nonce)
})
t.Run("ok - custom options", func(t *testing.T) {
ctrl := gomock.NewController(t)
specialType := ssi.MustParseURI("SpecialPresentation")
+ domain := "https://example.com"
+ nonce := "the-nonce"
options := PresentationOptions{
AdditionalContexts: []ssi.URI{credential.NutsV1ContextURI},
AdditionalTypes: []ssi.URI{specialType},
ProofOptions: proof.ProofOptions{
ProofPurpose: "authentication",
+ Domain: &domain,
+ Nonce: &nonce,
},
Format: JSONLDPresentationFormat,
}
@@ -113,9 +120,12 @@ func TestWallet_BuildPresentation(t *testing.T) {
require.NotNil(t, result)
assert.True(t, result.IsType(specialType))
assert.True(t, result.ContainsContext(credential.NutsV1ContextURI))
- proofs, _ := result.Proofs()
+ var proofs []proof.LDProof
+ require.NoError(t, result.UnmarshalProofValue(&proofs))
require.Len(t, proofs, 1)
- assert.Equal(t, proofs[0].ProofPurpose, "authentication")
+ assert.Equal(t, "authentication", proofs[0].ProofPurpose)
+ assert.Equal(t, "https://example.com", *proofs[0].Domain)
+ assert.Equal(t, nonce, *proofs[0].Nonce)
assert.Equal(t, JSONLDPresentationFormat, result.Format())
})
t.Run("ok - multiple VCs", func(t *testing.T) {
@@ -152,6 +162,8 @@ func TestWallet_BuildPresentation(t *testing.T) {
assert.NotEmpty(t, result.ID.Fragment, "id must have a fragment")
assert.Equal(t, JWTPresentationFormat, result.Format())
assert.NotNil(t, result.JWT())
+ nonce, _ := result.JWT().Get("nonce")
+ assert.Empty(t, nonce)
})
t.Run("ok - multiple VCs", func(t *testing.T) {
ctrl := gomock.NewController(t)
@@ -171,11 +183,15 @@ func TestWallet_BuildPresentation(t *testing.T) {
})
t.Run("optional proof options", func(t *testing.T) {
exp := time.Now().Local().Truncate(time.Second)
+ domain := "https://example.com"
+ nonce := "the-nonce"
options := PresentationOptions{
Format: JWTPresentationFormat,
ProofOptions: proof.ProofOptions{
Expires: &exp,
Created: exp.Add(-1 * time.Hour),
+ Domain: &domain,
+ Nonce: &nonce,
},
}
@@ -194,6 +210,9 @@ func TestWallet_BuildPresentation(t *testing.T) {
assert.NotNil(t, result.JWT())
assert.Equal(t, *options.ProofOptions.Expires, result.JWT().Expiration().Local())
assert.Equal(t, options.ProofOptions.Created, result.JWT().NotBefore().Local())
+ assert.Equal(t, []string{domain}, result.JWT().Audience())
+ actualNonce, _ := result.JWT().Get("nonce")
+ assert.Equal(t, nonce, actualNonce)
})
})
t.Run("validation", func(t *testing.T) {
diff --git a/vcr/pe/presentation_definition_test.go b/vcr/pe/presentation_definition_test.go
index 431d264d6a..e8e5515d37 100644
--- a/vcr/pe/presentation_definition_test.go
+++ b/vcr/pe/presentation_definition_test.go
@@ -24,6 +24,7 @@ import (
"crypto/rand"
"embed"
"encoding/json"
+ "github.com/nuts-foundation/go-did/did"
"github.com/nuts-foundation/nuts-node/vcr/credential"
"testing"
@@ -89,7 +90,7 @@ func definitions() testDefinitions {
func TestMatch(t *testing.T) {
jsonldVC := credential.ValidNutsOrganizationCredential(t)
- jwtVC := credential.JWTNutsOrganizationCredential(t)
+ jwtVC := credential.JWTNutsOrganizationCredential(t, did.MustParseDID("did:web:example.com"))
t.Run("Basic", func(t *testing.T) {
t.Run("JSON-LD", func(t *testing.T) {
@@ -671,8 +672,9 @@ func Test_matchFilter(t *testing.T) {
}
func TestPresentationDefinition_ResolveConstraintsFields(t *testing.T) {
- jwtCredential := credential.JWTNutsOrganizationCredential(t)
- jsonldCredential := credential.JWTNutsOrganizationCredential(t)
+ subjectDID := did.MustParseDID("did:web:example.com")
+ jwtCredential := credential.JWTNutsOrganizationCredential(t, subjectDID)
+ jsonldCredential := credential.JWTNutsOrganizationCredential(t, subjectDID)
definition := definitions().JSONLDorJWT
t.Run("match JWT", func(t *testing.T) {
credentialMap := map[string]vc.VerifiableCredential{
diff --git a/vcr/pe/test.go b/vcr/pe/test.go
new file mode 100644
index 0000000000..7930e11657
--- /dev/null
+++ b/vcr/pe/test.go
@@ -0,0 +1,30 @@
+/*
+ * 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/require"
+ "testing"
+)
+
+func TestDefinitionResolver(t testing.TB) *DefinitionResolver {
+ peStore := &DefinitionResolver{}
+ require.NoError(t, peStore.LoadFromFile("test/presentation_definition_mapping.json"))
+ return peStore
+}
diff --git a/vcr/pe/util_test.go b/vcr/pe/util_test.go
index 291e8f0054..4b027a6c47 100644
--- a/vcr/pe/util_test.go
+++ b/vcr/pe/util_test.go
@@ -30,7 +30,7 @@ import (
func TestParseEnvelope(t *testing.T) {
t.Run("JWT", func(t *testing.T) {
- presentation := test.CreateJWTPresentation(t, did.MustParseDID("did:example:1"), credential.ValidNutsOrganizationCredential(t))
+ presentation := test.CreateJWTPresentation(t, did.MustParseDID("did:example:1"), nil, credential.ValidNutsOrganizationCredential(t))
envelope, err := ParseEnvelope([]byte(presentation.Raw()))
require.NoError(t, err)
require.Equal(t, presentation.ID.String(), envelope.asInterface.(map[string]interface{})["id"])
@@ -59,7 +59,7 @@ func TestParseEnvelope(t *testing.T) {
require.Len(t, envelope.Presentations, 1)
})
t.Run("JSON array with JWTs", func(t *testing.T) {
- presentation := test.CreateJWTPresentation(t, did.MustParseDID("did:example:1"), credential.ValidNutsOrganizationCredential(t))
+ presentation := test.CreateJWTPresentation(t, did.MustParseDID("did:example:1"), nil, credential.ValidNutsOrganizationCredential(t))
presentations := []string{presentation.Raw(), presentation.Raw()}
listJSON, _ := json.Marshal(presentations)
envelope, err := ParseEnvelope(listJSON)
diff --git a/vcr/signature/proof/jsonld.go b/vcr/signature/proof/jsonld.go
index 124ff075fd..ff5b71fd2b 100644
--- a/vcr/signature/proof/jsonld.go
+++ b/vcr/signature/proof/jsonld.go
@@ -62,6 +62,8 @@ type ProofOptions struct {
// ProofPurpose contains a 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 string `json:"proofPurpose"`
+ // Nonce contains a value that is used to prevent replay attacks
+ Nonce *string `json:"nonce,omitempty"`
}
// ValidAt checks if the proof is valid at a certain given time.
@@ -81,7 +83,6 @@ func (o ProofOptions) ValidAt(at time.Time, maxSkew time.Duration) bool {
// LDProof contains the fields of the Proof data model: https://w3c-ccg.github.io/data-integrity-spec/#proofs
type LDProof struct {
ProofOptions
- Nonce *string `json:"nonce,omitempty"`
// Type contains the signature type. Its is determined from the key type.
Type ssi.ProofType `json:"type"`
// VerificationMethod is the key identifier for the public/private key pair used to sign this proof
diff --git a/vcr/test/test.go b/vcr/test/test.go
index 9699576e45..ceb1793be9 100644
--- a/vcr/test/test.go
+++ b/vcr/test/test.go
@@ -29,13 +29,15 @@ 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/crypto"
+ "github.com/nuts-foundation/nuts-node/vcr/signature/proof"
"github.com/stretchr/testify/require"
"testing"
"time"
)
// CreateJWTPresentation creates a JWT presentation with the given subject DID and credentials.
-func CreateJWTPresentation(t *testing.T, subjectDID did.DID, credentials ...vc.VerifiableCredential) vc.VerifiablePresentation {
+func CreateJWTPresentation(t *testing.T, subjectDID did.DID, tokenVisitor func(token jwt.Token), credentials ...vc.VerifiableCredential) vc.VerifiablePresentation {
headers := jws.NewHeaders()
require.NoError(t, headers.Set(jws.TypeKey, "JWT"))
claims := map[string]interface{}{
@@ -43,7 +45,8 @@ func CreateJWTPresentation(t *testing.T, subjectDID did.DID, credentials ...vc.V
jwt.SubjectKey: subjectDID.String(),
jwt.JwtIDKey: subjectDID.String() + "#" + uuid.NewString(),
jwt.NotBeforeKey: time.Now().Unix(),
- jwt.ExpirationKey: time.Now().Add(time.Hour).Unix(),
+ jwt.ExpirationKey: time.Now().Add(5 * time.Second).Unix(),
+ "nonce": crypto.GenerateNonce(),
"vp": vc.VerifiablePresentation{
Type: []ssi.URI{vc.VerifiablePresentationTypeV1URI()},
VerifiableCredential: credentials,
@@ -53,9 +56,57 @@ func CreateJWTPresentation(t *testing.T, subjectDID did.DID, credentials ...vc.V
for k, v := range claims {
require.NoError(t, unsignedToken.Set(k, v))
}
+ if tokenVisitor != nil {
+ tokenVisitor(unsignedToken)
+ }
privateKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
token, _ := jwt.Sign(unsignedToken, jwt.WithKey(jwa.ES256, privateKey, jws.WithProtectedHeaders(headers)))
result, err := vc.ParseVerifiablePresentation(string(token))
require.NoError(t, err)
return *result
}
+
+// CreateJSONLDPresentation creates a JSON-LD presentation with the given subject DID and credentials.
+// The presentation is not actually signed.
+func CreateJSONLDPresentation(t *testing.T, subjectDID did.DID, visitor func(presentation *vc.VerifiablePresentation), verifiableCredential ...vc.VerifiableCredential) vc.VerifiablePresentation {
+ id := ssi.MustParseURI(subjectDID.String() + "#" + uuid.NewString())
+ exp := time.Now().Add(5 * time.Second)
+ nonce := crypto.GenerateNonce()
+ vp := vc.VerifiablePresentation{
+ ID: &id,
+ VerifiableCredential: verifiableCredential,
+ Proof: []interface{}{
+ proof.LDProof{
+ Type: ssi.JsonWebSignature2020,
+ VerificationMethod: ssi.MustParseURI(subjectDID.String() + "#1"),
+ ProofOptions: proof.ProofOptions{
+ Created: time.Now(),
+ Expires: &exp,
+ Nonce: &nonce,
+ },
+ },
+ },
+ }
+ if visitor != nil {
+ visitor(&vp)
+ }
+ return ParsePresentation(t, vp)
+}
+
+// LDProofVisitor is a util function that creates a visitor for CreateJSONLDPresentation to easily modify the LinkedData proof.
+func LDProofVisitor(visitor func(proof *proof.LDProof)) func(*vc.VerifiablePresentation) {
+ return func(presentation *vc.VerifiablePresentation) {
+ ldProof := presentation.Proof[0].(proof.LDProof)
+ visitor(&ldProof)
+ presentation.Proof[0] = ldProof
+ }
+}
+
+// ParsePresentation marshals the given presentation and parses it again, to make sure the format property is set correctly.
+func ParsePresentation(t *testing.T, presentation vc.VerifiablePresentation) vc.VerifiablePresentation {
+ data, err := presentation.MarshalJSON()
+ require.NoError(t, err)
+ result, err := vc.ParseVerifiablePresentation(string(data))
+ require.NoError(t, err)
+ return *result
+}
diff --git a/vcr/verifier/verifier.go b/vcr/verifier/verifier.go
index e52325e420..31df90d8be 100644
--- a/vcr/verifier/verifier.go
+++ b/vcr/verifier/verifier.go
@@ -328,12 +328,10 @@ func (v *verifier) validateJSONLDPresentation(presentation vc.VerifiablePresenta
return newVerificationError("exactly 1 proof is expected")
}
// Make sure the proofs are LD-proofs
- var ldProofs []proof.LDProof
- err := presentation.UnmarshalProofValue(&ldProofs)
+ ldProof, err := credential.ParseLDProof(presentation)
if err != nil {
return newVerificationError("unsupported proof type: %w", err)
}
- ldProof := ldProofs[0]
// Validate signing time
at := timeFunc()
@@ -377,7 +375,7 @@ func (v *verifier) validateJWTPresentation(presentation vc.VerifiablePresentatio
return time.Now()
}
return *at
- })))
+ })), jwt.WithAcceptableSkew(maxSkew))
if err != nil {
return fmt.Errorf("unable to validate JWT credential: %w", err)
}
diff --git a/vcr/verifier/verifier_test.go b/vcr/verifier/verifier_test.go
index 964ee07082..2b18e30c04 100644
--- a/vcr/verifier/verifier_test.go
+++ b/vcr/verifier/verifier_test.go
@@ -744,7 +744,7 @@ func TestVerifier_VerifyVP(t *testing.T) {
vcs, err := ctx.verifier.VerifyVP(vp, false, false, validAt)
- assert.EqualError(t, err, "verification error: unsupported proof type: json: cannot unmarshal string into Go value of type proof.LDProof")
+ assert.EqualError(t, err, "verification error: unsupported proof type: invalid LD-proof for presentation: json: cannot unmarshal string into Go value of type proof.LDProof")
assert.Empty(t, vcs)
})
t.Run("error - no proof", func(t *testing.T) {
diff --git a/vdr/cmd/cmd.go b/vdr/cmd/cmd.go
index 6ff996c9ae..b921d58dad 100644
--- a/vdr/cmd/cmd.go
+++ b/vdr/cmd/cmd.go
@@ -82,7 +82,7 @@ func createCmd() *cobra.Command {
result := &cobra.Command{
Use: "create-did",
Short: "Registers a new DID",
- Long: "When using the V2 API, a web:did will be created. All the other options are ignored for a web:did.",
+ Long: "When using the V2 API, a did:web DID will be created. All the other options are ignored for did:web.",
Args: cobra.ExactArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
clientConfig := core.NewClientConfigForCommand(cmd)
@@ -118,7 +118,7 @@ func createCmd() *cobra.Command {
result.Flags().BoolVar(createRequest.CapabilityInvocation, "capabilityInvocation", defs.KeyFlags.Is(management.CapabilityInvocationUsage), setUsage(defs.KeyFlags.Is(management.CapabilityInvocationUsage), "Pass '%t' to %s capabilityInvocation capabilities."))
result.Flags().BoolVar(createRequest.KeyAgreement, "keyAgreement", defs.KeyFlags.Is(management.KeyAgreementUsage), setUsage(defs.KeyFlags.Is(management.KeyAgreementUsage), "Pass '%t' to %s keyAgreement capabilities."))
result.Flags().BoolVar(createRequest.SelfControl, "selfControl", defs.SelfControl, setUsage(defs.SelfControl, "Pass '%t' to %s DID Document control."))
- result.Flags().BoolVar(&useV2, "v2", false, "Pass 'true' to use the V2 API and create a web:did.")
+ result.Flags().BoolVar(&useV2, "v2", false, "Pass 'true' to use the V2 API and create a did:web DID.")
result.Flags().StringSliceVar(createRequest.Controllers, "controllers", []string{}, "Comma-separated list of DIDs that can control the generated DID Document.")
return result
diff --git a/vdr/management/management_mock.go b/vdr/management/management_mock.go
index 6ae0dbd41f..f3d5f10cc2 100644
--- a/vdr/management/management_mock.go
+++ b/vdr/management/management_mock.go
@@ -14,138 +14,9 @@ import (
did "github.com/nuts-foundation/go-did/did"
crypto "github.com/nuts-foundation/nuts-node/crypto"
- resolver "github.com/nuts-foundation/nuts-node/vdr/resolver"
gomock "go.uber.org/mock/gomock"
)
-// MockManager is a mock of Manager interface.
-type MockManager struct {
- ctrl *gomock.Controller
- recorder *MockManagerMockRecorder
-}
-
-// MockManagerMockRecorder is the mock recorder for MockManager.
-type MockManagerMockRecorder struct {
- mock *MockManager
-}
-
-// NewMockManager creates a new mock instance.
-func NewMockManager(ctrl *gomock.Controller) *MockManager {
- mock := &MockManager{ctrl: ctrl}
- mock.recorder = &MockManagerMockRecorder{mock}
- return mock
-}
-
-// EXPECT returns an object that allows the caller to indicate expected use.
-func (m *MockManager) EXPECT() *MockManagerMockRecorder {
- return m.recorder
-}
-
-// AddVerificationMethod mocks base method.
-func (m *MockManager) AddVerificationMethod(ctx context.Context, id did.DID, keyUsage DIDKeyFlags) (*did.VerificationMethod, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "AddVerificationMethod", ctx, id, keyUsage)
- ret0, _ := ret[0].(*did.VerificationMethod)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// AddVerificationMethod indicates an expected call of AddVerificationMethod.
-func (mr *MockManagerMockRecorder) AddVerificationMethod(ctx, id, keyUsage any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddVerificationMethod", reflect.TypeOf((*MockManager)(nil).AddVerificationMethod), ctx, id, keyUsage)
-}
-
-// Create mocks base method.
-func (m *MockManager) Create(ctx context.Context, method string, options DIDCreationOptions) (*did.Document, crypto.Key, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "Create", ctx, method, options)
- ret0, _ := ret[0].(*did.Document)
- ret1, _ := ret[1].(crypto.Key)
- ret2, _ := ret[2].(error)
- return ret0, ret1, ret2
-}
-
-// Create indicates an expected call of Create.
-func (mr *MockManagerMockRecorder) Create(ctx, method, options any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockManager)(nil).Create), ctx, method, options)
-}
-
-// Deactivate mocks base method.
-func (m *MockManager) Deactivate(ctx context.Context, id did.DID) error {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "Deactivate", ctx, id)
- ret0, _ := ret[0].(error)
- return ret0
-}
-
-// Deactivate indicates an expected call of Deactivate.
-func (mr *MockManagerMockRecorder) Deactivate(ctx, id any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Deactivate", reflect.TypeOf((*MockManager)(nil).Deactivate), ctx, id)
-}
-
-// IsOwner mocks base method.
-func (m *MockManager) IsOwner(arg0 context.Context, arg1 did.DID) (bool, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "IsOwner", arg0, arg1)
- ret0, _ := ret[0].(bool)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// IsOwner indicates an expected call of IsOwner.
-func (mr *MockManagerMockRecorder) IsOwner(arg0, arg1 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsOwner", reflect.TypeOf((*MockManager)(nil).IsOwner), arg0, arg1)
-}
-
-// ListOwned mocks base method.
-func (m *MockManager) ListOwned(ctx context.Context) ([]did.DID, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ListOwned", ctx)
- ret0, _ := ret[0].([]did.DID)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// ListOwned indicates an expected call of ListOwned.
-func (mr *MockManagerMockRecorder) ListOwned(ctx any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListOwned", reflect.TypeOf((*MockManager)(nil).ListOwned), ctx)
-}
-
-// RemoveVerificationMethod mocks base method.
-func (m *MockManager) RemoveVerificationMethod(ctx context.Context, id did.DID, keyID did.DIDURL) error {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "RemoveVerificationMethod", ctx, id, keyID)
- ret0, _ := ret[0].(error)
- return ret0
-}
-
-// RemoveVerificationMethod indicates an expected call of RemoveVerificationMethod.
-func (mr *MockManagerMockRecorder) RemoveVerificationMethod(ctx, id, keyID any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveVerificationMethod", reflect.TypeOf((*MockManager)(nil).RemoveVerificationMethod), ctx, id, keyID)
-}
-
-// Resolve mocks base method.
-func (m *MockManager) Resolve(id did.DID, metadata *resolver.ResolveMetadata) (*did.Document, *resolver.DocumentMetadata, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "Resolve", id, metadata)
- ret0, _ := ret[0].(*did.Document)
- ret1, _ := ret[1].(*resolver.DocumentMetadata)
- ret2, _ := ret[2].(error)
- return ret0, ret1, ret2
-}
-
-// Resolve indicates an expected call of Resolve.
-func (mr *MockManagerMockRecorder) Resolve(id, metadata any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Resolve", reflect.TypeOf((*MockManager)(nil).Resolve), id, metadata)
-}
-
// MockDocCreator is a mock of DocCreator interface.
type MockDocCreator struct {
ctrl *gomock.Controller
@@ -170,9 +41,9 @@ func (m *MockDocCreator) EXPECT() *MockDocCreatorMockRecorder {
}
// Create mocks base method.
-func (m *MockDocCreator) Create(ctx context.Context, method string, options DIDCreationOptions) (*did.Document, crypto.Key, error) {
+func (m *MockDocCreator) Create(ctx context.Context, options DIDCreationOptions) (*did.Document, crypto.Key, error) {
m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "Create", ctx, method, options)
+ ret := m.ctrl.Call(m, "Create", ctx, options)
ret0, _ := ret[0].(*did.Document)
ret1, _ := ret[1].(crypto.Key)
ret2, _ := ret[2].(error)
@@ -180,9 +51,9 @@ func (m *MockDocCreator) Create(ctx context.Context, method string, options DIDC
}
// Create indicates an expected call of Create.
-func (mr *MockDocCreatorMockRecorder) Create(ctx, method, options any) *gomock.Call {
+func (mr *MockDocCreatorMockRecorder) Create(ctx, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockDocCreator)(nil).Create), ctx, method, options)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockDocCreator)(nil).Create), ctx, options)
}
// MockDocUpdater is a mock of DocUpdater interface.