Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

allow local issuance for did:web #2641

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions vcr/issuer/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
52 changes: 49 additions & 3 deletions vcr/issuer/issuer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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},
Expand All @@ -71,6 +75,8 @@ func NewIssuer(store Store, vcrStore types.Writer, networkPublisher Publisher,
jsonldManager: jsonldManager,
trustConfig: trustConfig,
vcrStore: vcrStore,
documentOwner: documentOwner,
wallet: wallet,
}
}

Expand All @@ -85,14 +91,16 @@ 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.
// If publish is true, it publishes the credential to the network using the configured Publisher
// Use the public flag to pass the visibility settings to the Publisher.
func (i issuer) Issue(ctx context.Context, 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")
}

Expand Down Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but JWTs can't be loaded into the local wallet this way?

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) {
Expand Down Expand Up @@ -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")
Expand Down
55 changes: 48 additions & 7 deletions vcr/issuer/issuer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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())
Expand All @@ -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) {
Expand All @@ -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())
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}

Expand Down
10 changes: 6 additions & 4 deletions vcr/vcr.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion vcr/verifier/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down
15 changes: 7 additions & 8 deletions vcr/verifier/verifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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")
Expand Down
Loading