From 57deb4f1d67ab911c30685d3f1e40668f07188ad Mon Sep 17 00:00:00 2001 From: Wout Slakhorst Date: Fri, 1 Dec 2023 17:13:41 +0100 Subject: [PATCH] allow local issuance for did:web --- vcr/issuer/interface.go | 7 ----- vcr/issuer/issuer.go | 52 +++++++++++++++++++++++++++++++++--- vcr/issuer/issuer_test.go | 55 ++++++++++++++++++++++++++++++++++----- vcr/vcr.go | 10 ++++--- vcr/verifier/interface.go | 2 +- vcr/verifier/verifier.go | 15 +++++------ 6 files changed, 111 insertions(+), 30 deletions(-) diff --git a/vcr/issuer/interface.go b/vcr/issuer/interface.go index f8c3a54324..e760cec393 100644 --- a/vcr/issuer/interface.go +++ b/vcr/issuer/interface.go @@ -85,13 +85,6 @@ type CredentialSearcher interface { SearchCredential(credentialType ssi.URI, issuer did.DID, subject *ssi.URI) ([]vc.VerifiableCredential, error) } -const ( - JSONLDCredentialFormat = vc.JSONLDCredentialProofFormat - JWTCredentialFormat = vc.JWTCredentialProofFormat - JSONLDPresentationFormat = vc.JSONLDPresentationProofFormat - JWTPresentationFormat = vc.JWTPresentationProofFormat -) - // CredentialOptions specifies options for issuing a credential. type CredentialOptions struct { // Format specifies the proof format for the issued credential. If not set, it defaults to JSON-LD. diff --git a/vcr/issuer/issuer.go b/vcr/issuer/issuer.go index 620d5a02c6..608c618fc3 100644 --- a/vcr/issuer/issuer.go +++ b/vcr/issuer/issuer.go @@ -23,7 +23,10 @@ import ( "encoding/json" "errors" "fmt" + "github.com/nuts-foundation/nuts-node/vcr/holder" "github.com/nuts-foundation/nuts-node/vcr/openid4vci" + "github.com/nuts-foundation/nuts-node/vdr/didweb" + "github.com/nuts-foundation/nuts-node/vdr/management" "github.com/nuts-foundation/nuts-node/vdr/resolver" "time" @@ -54,6 +57,7 @@ var TimeFunc = time.Now func NewIssuer(store Store, vcrStore types.Writer, networkPublisher Publisher, openidHandlerFn func(ctx context.Context, id did.DID) (OpenIDHandler, error), didResolver resolver.DIDResolver, keyStore crypto.KeyStore, jsonldManager jsonld.JSONLD, trustConfig *trust.Config, + documentOwner management.DocumentOwner, wallet holder.Wallet, ) Issuer { keyResolver := vdrKeyResolver{ publicKeyResolver: resolver.DIDKeyResolver{Resolver: didResolver}, @@ -71,6 +75,8 @@ func NewIssuer(store Store, vcrStore types.Writer, networkPublisher Publisher, jsonldManager: jsonldManager, trustConfig: trustConfig, vcrStore: vcrStore, + documentOwner: documentOwner, + wallet: wallet, } } @@ -85,6 +91,8 @@ type issuer struct { jsonldManager jsonld.JSONLD vcrStore types.Writer walletResolver openid4vci.IdentifierResolver + documentOwner management.DocumentOwner + wallet holder.Wallet } // Issue creates a new credential, signs, stores it. @@ -92,7 +100,7 @@ type issuer struct { // Use the public flag to pass the visibility settings to the Publisher. func (i issuer) Issue(ctx context.Context, template vc.VerifiableCredential, options CredentialOptions) (*vc.VerifiableCredential, error) { // Until further notice we don't support publishing JWT VCs, since they're not officially supported by Nuts yet. - if options.Publish && options.Format == JWTCredentialFormat { + if options.Publish && options.Format == vc.JWTCredentialProofFormat { return nil, errors.New("publishing VC JWTs is not supported") } @@ -155,9 +163,47 @@ func (i issuer) Issue(ctx context.Context, template vc.VerifiableCredential, opt return nil, fmt.Errorf("unable to publish the issued credential: %w", err) } } + // local to local wallet for did:web + i.tryLocalIssuance(ctx, options, createdVC) + return createdVC, nil } +func (i issuer) tryLocalIssuance(ctx context.Context, options CredentialOptions, createdVC *vc.VerifiableCredential) { + if options.Publish { + // not supported, return silently + return + } + if options.Public { + // not supported, return silently + return + } + subject, err := createdVC.SubjectDID() + if err != nil { + log.Logger().Debug("Unable to determine subject DID, skipping local issuance") + return + } + if subject.Method != didweb.MethodName { + // not supported, return silently + return + } + isOwned, err := i.documentOwner.IsOwner(ctx, *subject) + if err != nil { + log.Logger().Debug("Unable to determine if subject DID is owned by this node, skipping local issuance") + return + } + if !isOwned { + // not supported, return silently + return + } + // put in wallet + if err := i.wallet.Put(ctx, *createdVC); err != nil { + // todo: at one point this becomes mainstream and the error needs to be returned + log.Logger().Error("Unable to put credential in wallet, skipping local issuance") + return + } +} + // issueUsingOpenID4VCI tries to issue the credential over OpenID4VCI. It returns whether the credential was offered successfully. // If no error is returned and bool is false, it means the wallet does not support OpenID4VCI. func (i issuer) issueUsingOpenID4VCI(ctx context.Context, credential vc.VerifiableCredential) (bool, error) { @@ -228,13 +274,13 @@ func (i issuer) buildVC(ctx context.Context, template vc.VerifiableCredential, o } switch options.Format { - case JWTCredentialFormat: + case vc.JWTCredentialProofFormat: return vc.CreateJWTVerifiableCredential(ctx, unsignedCredential, func(ctx context.Context, claims map[string]interface{}, headers map[string]interface{}) (string, error) { return i.keyStore.SignJWT(ctx, claims, headers, key) }) case "": fallthrough - case JSONLDCredentialFormat: + case vc.JSONLDCredentialProofFormat: return i.buildJSONLDCredential(ctx, unsignedCredential, key) default: return nil, errors.New("unsupported credential proof format") diff --git a/vcr/issuer/issuer_test.go b/vcr/issuer/issuer_test.go index 9f3cc9e30e..6e1391ecb3 100644 --- a/vcr/issuer/issuer_test.go +++ b/vcr/issuer/issuer_test.go @@ -26,7 +26,9 @@ import ( "fmt" "github.com/nuts-foundation/nuts-node/audit" "github.com/nuts-foundation/nuts-node/core" + "github.com/nuts-foundation/nuts-node/vcr/holder" "github.com/nuts-foundation/nuts-node/vcr/openid4vci" + "github.com/nuts-foundation/nuts-node/vdr/management" "github.com/nuts-foundation/nuts-node/vdr/resolver" "github.com/stretchr/testify/require" "path" @@ -86,11 +88,11 @@ func Test_issuer_buildVC(t *testing.T) { jsonldManager := jsonld.NewTestJSONLDManager(t) sut := issuer{keyResolver: keyResolverMock, jsonldManager: jsonldManager, keyStore: keyStore} - result, err := sut.buildVC(ctx, template, CredentialOptions{Format: JSONLDCredentialFormat}) + result, err := sut.buildVC(ctx, template, CredentialOptions{Format: vc.JSONLDCredentialProofFormat}) require.NoError(t, err) require.NotNil(t, result) assert.Contains(t, result.Type, credentialType, "expected vc to be of right type") - assert.Equal(t, JSONLDCredentialFormat, result.Format()) + assert.Equal(t, vc.JSONLDCredentialProofFormat, result.Format()) assert.Equal(t, issuerID.String(), result.Issuer.String(), "expected correct issuer") assert.Contains(t, result.Context, schemaOrgContext) assert.Contains(t, result.Context, vc.VCContextV1URI()) @@ -110,7 +112,7 @@ func Test_issuer_buildVC(t *testing.T) { result, err := sut.buildVC(ctx, template, CredentialOptions{}) require.NoError(t, err) require.NotNil(t, result) - assert.Equal(t, JSONLDCredentialFormat, result.Format()) + assert.Equal(t, vc.JSONLDCredentialProofFormat, result.Format()) }) }) t.Run("JWT", func(t *testing.T) { @@ -121,11 +123,11 @@ func Test_issuer_buildVC(t *testing.T) { jsonldManager := jsonld.NewTestJSONLDManager(t) sut := issuer{keyResolver: keyResolverMock, jsonldManager: jsonldManager, keyStore: keyStore} - result, err := sut.buildVC(ctx, template, CredentialOptions{Format: JWTCredentialFormat}) + result, err := sut.buildVC(ctx, template, CredentialOptions{Format: vc.JWTCredentialProofFormat}) require.NoError(t, err) require.NotNil(t, result) - assert.Equal(t, JWTCredentialFormat, result.Format()) + assert.Equal(t, vc.JWTCredentialProofFormat, result.Format()) assert.Contains(t, result.Type, credentialType, "expected vc to be of right type") assert.Contains(t, result.Context, schemaOrgContext) assert.Contains(t, result.Context, vc.VCContextV1URI()) @@ -290,7 +292,7 @@ func Test_issuer_Issue(t *testing.T) { result, err := sut.Issue(ctx, template, CredentialOptions{ Publish: true, Public: true, - Format: JWTCredentialFormat, + Format: vc.JWTCredentialProofFormat, }) require.EqualError(t, err, "publishing VC JWTs is not supported") assert.Nil(t, result) @@ -509,10 +511,49 @@ func Test_issuer_Issue(t *testing.T) { assert.Nil(t, result) }) }) + + t.Run("local issuer", func(t *testing.T) { + issuerDID := did.MustParseDID("did:web:nuts.test:iam:123") + issuerKeyID := issuerDID.String() + "#abc" + holderDID := did.MustParseDID("did:web:nuts.test:iam:456") + + template := vc.VerifiableCredential{ + Context: []ssi.URI{credential.NutsV1ContextURI}, + Type: []ssi.URI{credentialType}, + Issuer: issuerDID.URI(), + CredentialSubject: []interface{}{map[string]interface{}{ + "id": holderDID.String(), + }}, + } + ctrl := gomock.NewController(t) + + trustConfig := trust.NewConfig(path.Join(io.TestDirectory(t), "trust.config")) + keyResolverMock := NewMockkeyResolver(ctrl) + keyResolverMock.EXPECT().ResolveAssertionKey(ctx, gomock.Any()).Return(crypto.NewTestKey(issuerKeyID), nil) + mockStore := NewMockStore(ctrl) + mockStore.EXPECT().StoreCredential(gomock.Any()) + mockDocumentOwner := management.NewMockDocumentOwner(ctrl) + mockDocumentOwner.EXPECT().IsOwner(gomock.Any(), holderDID).Return(true, nil) + mockWallet := holder.NewMockWallet(ctrl) + mockWallet.EXPECT().Put(gomock.Any(), gomock.Any()).Return(nil) + sut := issuer{ + keyResolver: keyResolverMock, store: mockStore, + jsonldManager: jsonldManager, trustConfig: trustConfig, + keyStore: crypto.NewMemoryCryptoInstance(), + wallet: mockWallet, + documentOwner: mockDocumentOwner, + } + + _, err := sut.Issue(ctx, template, CredentialOptions{ + Publish: false, + Public: false, + }) + require.NoError(t, err) + }) } func TestNewIssuer(t *testing.T) { - createdIssuer := NewIssuer(nil, nil, nil, nil, nil, nil, nil, nil) + createdIssuer := NewIssuer(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil) assert.IsType(t, &issuer{}, createdIssuer) } diff --git a/vcr/vcr.go b/vcr/vcr.go index acd60de598..b72c2999b0 100644 --- a/vcr/vcr.go +++ b/vcr/vcr.go @@ -261,17 +261,19 @@ func (c *vcr) Configure(config core.ServerConfig) error { c.walletHttpClient = core.NewStrictHTTPClient(config.Strictmode, c.config.OpenID4VCI.Timeout, tlsConfig) c.openidSessionStore = c.storageClient.GetSessionDatabase() } - c.issuer = issuer.NewIssuer(c.issuerStore, c, networkPublisher, openidHandlerFn, didResolver, c.keyStore, c.jsonldManager, c.trustConfig) + // verifier, wallet and issuer order is important! + // verifier c.verifier = verifier.NewVerifier(c.verifierStore, didResolver, c.keyResolver, c.jsonldManager, c.trustConfig) - - c.ambassador = NewAmbassador(c.network, c, c.verifier, c.eventManager) - // Create holder/wallet c.walletStore, err = c.storageClient.GetProvider(ModuleName).GetKVStore("wallet", storage.PersistentStorageClass) if err != nil { return err } c.wallet = holder.New(c.keyResolver, c.keyStore, c.verifier, c.jsonldManager, c.walletStore) + // issuer + c.issuer = issuer.NewIssuer(c.issuerStore, c, networkPublisher, openidHandlerFn, didResolver, c.keyStore, c.jsonldManager, c.trustConfig, c.vdrInstance, c.wallet) + + c.ambassador = NewAmbassador(c.network, c, c.verifier, c.eventManager) if err = c.store.HandleRestore(); err != nil { return err diff --git a/vcr/verifier/interface.go b/vcr/verifier/interface.go index 793d92acf1..03c0a44ac5 100644 --- a/vcr/verifier/interface.go +++ b/vcr/verifier/interface.go @@ -20,12 +20,12 @@ package verifier import ( "errors" - "github.com/nuts-foundation/nuts-node/core" "io" "time" ssi "github.com/nuts-foundation/go-did" "github.com/nuts-foundation/go-did/vc" + "github.com/nuts-foundation/nuts-node/core" "github.com/nuts-foundation/nuts-node/vcr/credential" ) diff --git a/vcr/verifier/verifier.go b/vcr/verifier/verifier.go index e1b60fce94..af43958d68 100644 --- a/vcr/verifier/verifier.go +++ b/vcr/verifier/verifier.go @@ -23,22 +23,21 @@ import ( "encoding/json" "errors" "fmt" - "github.com/lestrrat-go/jwx/v2/jwt" - "github.com/nuts-foundation/nuts-node/crypto" - "github.com/nuts-foundation/nuts-node/vcr/issuer" - "github.com/nuts-foundation/nuts-node/vdr/resolver" "strings" "time" + "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/crypto" "github.com/nuts-foundation/nuts-node/jsonld" "github.com/nuts-foundation/nuts-node/vcr/credential" "github.com/nuts-foundation/nuts-node/vcr/signature" "github.com/nuts-foundation/nuts-node/vcr/signature/proof" "github.com/nuts-foundation/nuts-node/vcr/trust" "github.com/nuts-foundation/nuts-node/vcr/types" + "github.com/nuts-foundation/nuts-node/vdr/resolver" ) var timeFunc = time.Now @@ -118,9 +117,9 @@ func (v *verifier) Validate(credentialToVerify vc.VerifiableCredential, at *time } switch credentialToVerify.Format() { - case issuer.JSONLDCredentialFormat: + case vc.JSONLDCredentialProofFormat: return v.validateJSONLDCredential(credentialToVerify, at) - case issuer.JWTCredentialFormat: + case vc.JWTCredentialProofFormat: return v.validateJWTCredential(credentialToVerify, at) default: return errors.New("unsupported credential proof format") @@ -316,9 +315,9 @@ func (v verifier) VerifyVP(vp vc.VerifiablePresentation, verifyVCs bool, allowUn func (v verifier) doVerifyVP(vcVerifier Verifier, presentation vc.VerifiablePresentation, verifyVCs bool, allowUntrustedVCs bool, validAt *time.Time) ([]vc.VerifiableCredential, error) { var err error switch presentation.Format() { - case issuer.JSONLDPresentationFormat: + case vc.JSONLDPresentationProofFormat: err = v.validateJSONLDPresentation(presentation, validAt) - case issuer.JWTPresentationFormat: + case vc.JWTPresentationProofFormat: err = v.validateJWTPresentation(presentation, validAt) default: err = errors.New("unsupported presentation proof format")