Skip to content

Commit

Permalink
feat: vc wallet - derive credential (hyperledger-archives#2695)
Browse files Browse the repository at this point in the history
- Closes hyperledger-archives#2694

Signed-off-by: sudesh.shetty <sudesh.shetty@securekey.com>
  • Loading branch information
sudeshrshetty committed Oct 14, 2021
1 parent 7634bcf commit 4a50833
Show file tree
Hide file tree
Showing 6 changed files with 501 additions and 11 deletions.
10 changes: 10 additions & 0 deletions pkg/client/vcwallet/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,3 +247,13 @@ func (c *Client) Prove(opts *wallet.ProofOptions, creds ...wallet.CredentialToPr
func (c *Client) Verify(option wallet.VerificationOption) (bool, error) {
return c.wallet.Verify(option)
}

// Derive derives a credential and returns response credential.
//
// Args:
// - credential to derive (ID of the stored credential, raw credential or credential instance).
// - derive options.
//
func (c *Client) Derive(credential wallet.CredentialToDerive, options *wallet.DeriveOptions) (*verifiable.Credential, error) { //nolint: lll
return c.wallet.Derive(credential, options)
}
180 changes: 180 additions & 0 deletions pkg/client/vcwallet/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ package vcwallet

import (
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"strings"
Expand All @@ -16,7 +18,9 @@ import (
"github.com/google/uuid"
"github.com/stretchr/testify/require"

"github.com/hyperledger/aries-framework-go/pkg/crypto/tinkcrypto"
"github.com/hyperledger/aries-framework-go/pkg/doc/did"
"github.com/hyperledger/aries-framework-go/pkg/doc/verifiable"
vdrapi "github.com/hyperledger/aries-framework-go/pkg/framework/aries/api/vdr"
cryptomock "github.com/hyperledger/aries-framework-go/pkg/mock/crypto"
mockprovider "github.com/hyperledger/aries-framework-go/pkg/mock/provider"
Expand Down Expand Up @@ -134,6 +138,52 @@ const (
"type": ["VerifiableCredential", "UniversityDegreeCredential"]
}]
}`
sampleBBSVC = `{
"@context": ["https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1", "https://w3id.org/security/bbs/v1"],
"credentialSubject": {
"degree": {"type": "BachelorDegree", "university": "MIT"},
"id": "did:example:ebfeb1f712ebc6f1c276e12ec21",
"name": "Jayden Doe",
"spouse": "did:example:c276e12ec21ebfeb1f712ebc6f1"
},
"expirationDate": "2020-01-01T19:23:24Z",
"id": "http://example.edu/credentials/1872",
"issuanceDate": "2010-01-01T19:23:24Z",
"issuer": {"id": "did:example:76e12ec712ebc6f1c221ebfeb1f", "name": "Example University"},
"proof": {
"created": "2021-03-29T13:27:36.483097-04:00",
"proofPurpose": "assertionMethod",
"proofValue": "rw7FeV6K1wimnYogF9qd-N0zmq5QlaIoszg64HciTca-mK_WU4E1jIusKTT6EnN2GZz04NVPBIw4yhc0kTwIZ07etMvfWUlHt_KMoy2CfTw8FBhrf66q4h7Qcqxh_Kxp6yCHyB4A-MmURlKKb8o-4w",
"type": "BbsBlsSignature2020",
"verificationMethod": "did:key:zUC72c7u4BYVmfYinDceXkNAwzPEyuEE23kUmJDjLy8495KH3pjLwFhae1Fww9qxxRdLnS2VNNwni6W3KbYZKsicDtiNNEp76fYWR6HCD8jAz6ihwmLRjcHH6kB294Xfg1SL1qQ#zUC72c7u4BYVmfYinDceXkNAwzPEyuEE23kUmJDjLy8495KH3pjLwFhae1Fww9qxxRdLnS2VNNwni6W3KbYZKsicDtiNNEp76fYWR6HCD8jAz6ihwmLRjcHH6kB294Xfg1SL1qQ"
},
"referenceNumber": 83294847,
"type": ["VerifiableCredential", "UniversityDegreeCredential"]
}`

sampleFrame = `
{
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://www.w3.org/2018/credentials/examples/v1",
"https://w3id.org/security/bbs/v1"
],
"type": ["VerifiableCredential", "UniversityDegreeCredential"],
"@explicit": true,
"identifier": {},
"issuer": {},
"issuanceDate": {},
"credentialSubject": {
"@explicit": true,
"degree": {},
"name": {}
}
}
`
sampleInvalidDIDContent = `{
"@context": ["https://w3id.org/did/v1"],
"id": "did:example:sampleInvalidDIDContent"
}`
)

func TestCreateProfile(t *testing.T) {
Expand Down Expand Up @@ -829,6 +879,136 @@ func TestClient_Verify(t *testing.T) {
})
}

func TestWallet_Derive(t *testing.T) {
customVDR := &mockvdr.MockVDRegistry{
ResolveFunc: func(didID string, opts ...vdrapi.DIDMethodOption) (*did.DocResolution, error) {
if strings.HasPrefix(didID, "did:key:") {
k := key.New()

d, e := k.Read(didID)
if e != nil {
return nil, e
}

return d, nil
}

return nil, fmt.Errorf("did not found")
},
}

mockctx := newMockProvider()
mockctx.VDRegistryValue = customVDR

customCrypto, err := tinkcrypto.New()
require.NoError(t, err)

mockctx.CryptoValue = customCrypto

// create profile
err = CreateProfile(sampleUserID, mockctx, wallet.WithPassphrase(samplePassPhrase))
require.NoError(t, err)

// prepare frame
var frameDoc map[string]interface{}

require.NoError(t, json.Unmarshal([]byte(sampleFrame), &frameDoc))

t.Run("Test derive a credential from wallet - success", func(t *testing.T) {
walletInstance, err := New(sampleUserID, mockctx)
require.NoError(t, err)
require.NotEmpty(t, walletInstance)

// save BBS VC in store
require.NoError(t, walletInstance.Add(wallet.Credential, []byte(sampleBBSVC)))

sampleNonce := uuid.New().String()

verifyBBSProof := func(proofs []verifiable.Proof) {
require.Len(t, proofs, 1)
require.NotEmpty(t, proofs[0])
require.Equal(t, proofs[0]["type"], "BbsBlsSignatureProof2020")
require.NotEmpty(t, proofs[0]["nonce"])
require.EqualValues(t, proofs[0]["nonce"], base64.StdEncoding.EncodeToString([]byte(sampleNonce)))
require.NotEmpty(t, proofs[0]["proofValue"])
}

// derive stored credential
vc, err := walletInstance.Derive(wallet.FromStoredCredential("http://example.edu/credentials/1872"),
&wallet.DeriveOptions{
Nonce: sampleNonce,
Frame: frameDoc,
})
require.NoError(t, err)
require.NotEmpty(t, vc)
verifyBBSProof(vc.Proofs)

// derive raw credential
vc, err = walletInstance.Derive(wallet.FromRawCredential([]byte(sampleBBSVC)), &wallet.DeriveOptions{
Nonce: sampleNonce,
Frame: frameDoc,
})
require.NoError(t, err)
require.NotEmpty(t, vc)
verifyBBSProof(vc.Proofs)

// derive from credential instance
pkFetcher := verifiable.WithPublicKeyFetcher(
verifiable.NewVDRKeyResolver(customVDR).PublicKeyFetcher(),
)
credential, err := verifiable.ParseCredential([]byte(sampleBBSVC), pkFetcher)
require.NoError(t, err)
vc, err = walletInstance.Derive(wallet.FromCredential(credential), &wallet.DeriveOptions{
Nonce: sampleNonce,
Frame: frameDoc,
})
require.NoError(t, err)
require.NotEmpty(t, vc)
verifyBBSProof(vc.Proofs)
})

t.Run("Test derive credential failures", func(t *testing.T) {
walletInstance, err := New(sampleUserID, mockctx)
require.NotEmpty(t, walletInstance)
require.NoError(t, err)

// invalid request
vc, err := walletInstance.Derive(wallet.FromStoredCredential(""), &wallet.DeriveOptions{})
require.Empty(t, vc)
require.Error(t, err)
require.Contains(t, err.Error(), "invalid request to derive credential")

// credential not found in store
vc, err = walletInstance.Derive(wallet.FromStoredCredential("invalid-id"), &wallet.DeriveOptions{})
require.Empty(t, vc)
require.Error(t, err)
require.Contains(t, err.Error(), "data not found")

// invalid credential in store
require.NoError(t, walletInstance.Add(wallet.Credential, []byte(sampleInvalidDIDContent)))

vc, err = walletInstance.Derive(wallet.FromStoredCredential("did:example:sampleInvalidDIDContent"),
&wallet.DeriveOptions{})
require.Empty(t, vc)
require.Error(t, err)
require.Contains(t, err.Error(), "credential type of unknown structure")

// invalid raw credential
vc, err = walletInstance.Derive(wallet.FromRawCredential([]byte(sampleInvalidDIDContent)), &wallet.DeriveOptions{})
require.Empty(t, vc)
require.Error(t, err)
require.Contains(t, err.Error(), "credential type of unknown structure")

// try deriving wrong proof type - no BbsBlsSignature2020 proof present
vc, err = walletInstance.Derive(wallet.FromRawCredential([]byte(sampleUDCVCWithProof)), &wallet.DeriveOptions{
Frame: frameDoc,
})
require.Empty(t, vc)
require.Error(t, err)
require.Contains(t, err.Error(), "no BbsBlsSignature2020 proof present")
})
}

func newMockProvider() *mockprovider.Provider {
return &mockprovider.Provider{StorageProviderValue: mockstorage.NewMockStoreProvider()}
}
Expand Down
9 changes: 9 additions & 0 deletions pkg/wallet/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,12 @@ type ProofOptions struct {
// Optional, by default proof will be represented as 'verifiable.SignatureProofValue'.
ProofRepresentation *verifiable.SignatureRepresentation `json:"proofRepresentation,omitempty"`
}

// DeriveOptions model containing options for deriving a credential.
//
type DeriveOptions struct {
// Frame is JSON-LD frame used for selective disclosure.
Frame map[string]interface{} `json:"frame,omitempty"`
// Nonce to prove uniqueness or freshness of the proof.
Nonce string `json:"nonce,omitempty"`
}
34 changes: 34 additions & 0 deletions pkg/wallet/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,3 +167,37 @@ func WithRawPresentationToVerify(raw json.RawMessage) VerificationOption {
opts.rawPresentation = raw
}
}

// verifyOpts contains options for deriving credentials.
type deriveOpts struct {
// for deriving credential from stored credential.
credentialID string
// for deriving credential from raw credential.
rawCredential json.RawMessage
// for deriving credential from credential instance.
credential *verifiable.Credential
}

// CredentialToDerive is credential option for deriving a credential from wallet.
type CredentialToDerive func(opts *deriveOpts)

// FromStoredCredential for deriving credential from stored credential.
func FromStoredCredential(id string) CredentialToDerive {
return func(opts *deriveOpts) {
opts.credentialID = id
}
}

// FromRawCredential for deriving credential from raw credential bytes.
func FromRawCredential(raw json.RawMessage) CredentialToDerive {
return func(opts *deriveOpts) {
opts.rawCredential = raw
}
}

// FromCredential option for deriving credential from a credential instance.
func FromCredential(cred *verifiable.Credential) CredentialToDerive {
return func(opts *deriveOpts) {
opts.credential = cred
}
}
62 changes: 60 additions & 2 deletions pkg/wallet/wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ func (c *Wallet) Issue(authToken string, credential json.RawMessage,
// - proof options
//
func (c *Wallet) Prove(authToken string, proofOptions *ProofOptions, credentials ...CredentialToPresent) (*verifiable.Presentation, error) { //nolint: lll
resolved, err := c.resolveCredentials(credentials...)
resolved, err := c.resolveCredentialsToPresent(credentials...)
if err != nil {
return nil, fmt.Errorf("failed to resolve credentials from request: %w", err)
}
Expand Down Expand Up @@ -361,7 +361,32 @@ func (c *Wallet) Verify(options VerificationOption) (bool, error) {
}
}

func (c *Wallet) resolveCredentials(credentials ...CredentialToPresent) ([]*verifiable.Credential, error) {
// Derive derives a credential and returns response credential.
//
// Args:
// - credential to derive (ID of the stored credential, raw credential or credential instance).
// - derive options.
//
func (c *Wallet) Derive(credential CredentialToDerive,
options *DeriveOptions) (*verifiable.Credential, error) {
vc, err := c.resolveCredentialToDerive(credential)
if err != nil {
return nil, fmt.Errorf("failed to resolve request : %w", err)
}

pkFetcher := verifiable.WithPublicKeyFetcher(
verifiable.NewVDRKeyResolver(c.ctx.VDRegistry()).PublicKeyFetcher(),
)

derived, err := vc.GenerateBBSSelectiveDisclosure(options.Frame, []byte(options.Nonce), pkFetcher)
if err != nil {
return nil, fmt.Errorf("failed to derive credential : %w", err)
}

return derived, nil
}

func (c *Wallet) resolveCredentialsToPresent(credentials ...CredentialToPresent) ([]*verifiable.Credential, error) {
var response []*verifiable.Credential

opts := &proveOpts{}
Expand Down Expand Up @@ -408,6 +433,39 @@ func (c *Wallet) resolveCredentials(credentials ...CredentialToPresent) ([]*veri
return response, nil
}

func (c *Wallet) resolveCredentialToDerive(credential CredentialToDerive) (*verifiable.Credential, error) {
opts := &deriveOpts{}

credential(opts)

if opts.credential != nil {
return opts.credential, nil
}

if len(opts.rawCredential) > 0 {
// proof check is disabled while resolving credentials from store. A wallet UI may or may not choose to
// show credentials as verified. If a wallet implementation chooses to show credentials as 'verified' it
// may to call 'wallet.Verify()' for each credential being presented.
// (More details can be found in issue #2677).
return verifiable.ParseCredential(opts.rawCredential, verifiable.WithDisabledProofCheck())
}

if opts.credentialID != "" {
raw, err := c.contents.Get(Credential, opts.credentialID)
if err != nil {
return nil, err
}

// proof check is disabled while resolving credentials from store. A wallet UI may or may not choose to
// show credentials as verified. If a wallet implementation chooses to show credentials as 'verified' it
// may to call 'wallet.Verify()' for each credential being presented.
// (More details can be found in issue #2677).
return verifiable.ParseCredential(raw, verifiable.WithDisabledProofCheck())
}

return nil, errors.New("invalid request to derive credential")
}

func (c *Wallet) verifyCredential(credential json.RawMessage) (bool, error) {
// TODO resolve stored DID documents in wallet
opts := verifiable.WithPublicKeyFetcher(
Expand Down

0 comments on commit 4a50833

Please sign in to comment.