Skip to content

Commit

Permalink
allow local issuance for did:web
Browse files Browse the repository at this point in the history
  • Loading branch information
woutslakhorst committed Dec 1, 2023
1 parent 53b7172 commit 57deb4f
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 30 deletions.
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
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

0 comments on commit 57deb4f

Please sign in to comment.