diff --git a/cmd/aries-js-worker/src/agent-rest-client.js b/cmd/aries-js-worker/src/agent-rest-client.js index b4cd98a03..be97f7973 100644 --- a/cmd/aries-js-worker/src/agent-rest-client.js +++ b/cmd/aries-js-worker/src/agent-rest-client.js @@ -130,6 +130,10 @@ const pkgs = { path: "/verifiable/credentials", method: "GET", }, + SignCredential: { + path: "/verifiable/signcredential", + method: "POST" + }, GeneratePresentation: { path: "/verifiable/presentation/generate", method: "POST" diff --git a/cmd/aries-js-worker/src/aries.js b/cmd/aries-js-worker/src/aries.js index 902fd63ed..2a2b0eeb1 100644 --- a/cmd/aries-js-worker/src/aries.js +++ b/cmd/aries-js-worker/src/aries.js @@ -718,6 +718,16 @@ const Aries = function (opts) { return invoke(aw, pending, this.pkgname, "GetCredentials", {}, "timeout while retrieving verifiable credentials") }, + /** + * Signs and adds proof to given credential using provided proof options + * + * @param req - json document + * @returns {Promise} + */ + signCredential: async function (req) { + return invoke(aw, pending, this.pkgname, "SignCredential", req, "timeout while adding proof to credential") + }, + /** * Generates a verifiable presentation from a verifiable credential. * diff --git a/pkg/controller/command/verifiable/command.go b/pkg/controller/command/verifiable/command.go index c47c7f6b5..f4705488e 100644 --- a/pkg/controller/command/verifiable/command.go +++ b/pkg/controller/command/verifiable/command.go @@ -18,6 +18,7 @@ import ( "github.com/hyperledger/aries-framework-go/pkg/controller/internal/cmdutil" ariescrypto "github.com/hyperledger/aries-framework-go/pkg/crypto" "github.com/hyperledger/aries-framework-go/pkg/doc/did" + "github.com/hyperledger/aries-framework-go/pkg/doc/signature/jsonld" verifiablesigner "github.com/hyperledger/aries-framework-go/pkg/doc/signature/signer" "github.com/hyperledger/aries-framework-go/pkg/doc/signature/suite" "github.com/hyperledger/aries-framework-go/pkg/doc/signature/suite/ed25519signature2018" @@ -67,6 +68,9 @@ const ( // GetPresentationsErrorCode for get presentation records GetPresentationsErrorCode + + // SignCredentialErrorCode for sign credential error + SignCredentialErrorCode ) const ( @@ -79,6 +83,7 @@ const ( getCredentialCommandMethod = "GetCredential" getCredentialByNameCommandMethod = "GetCredentialByName" getCredentialsCommandMethod = "GetCredentials" + signCredentialCommandMethod = "SignCredential" savePresentationCommandMethod = "SavePresentation" getPresentationCommandMethod = "GetPresentation" getPresentationsCommandMethod = "GetPresentations" @@ -114,6 +119,10 @@ const ( Ed25519VerificationKey = "Ed25519VerificationKey" ) +type provable interface { + AddLinkedDataProof(context *verifiable.LinkedDataProofContext, jsonldOpts ...jsonld.ProcessorOpts) error +} + type keyResolver interface { PublicKeyFetcher() verifiable.PublicKeyFetcher } @@ -191,6 +200,7 @@ func (o *Command) GetHandlers() []command.Handler { cmdutil.NewCommandHandler(commandName, getCredentialCommandMethod, o.GetCredential), cmdutil.NewCommandHandler(commandName, getCredentialByNameCommandMethod, o.GetCredentialByName), cmdutil.NewCommandHandler(commandName, getCredentialsCommandMethod, o.GetCredentials), + cmdutil.NewCommandHandler(commandName, signCredentialCommandMethod, o.SignCredential), cmdutil.NewCommandHandler(commandName, generatePresentationCommandMethod, o.GeneratePresentation), cmdutil.NewCommandHandler(commandName, generatePresentationByIDCommandMethod, o.GeneratePresentationByID), cmdutil.NewCommandHandler(commandName, savePresentationCommandMethod, o.SavePresentation), @@ -343,6 +353,60 @@ func (o *Command) GetCredential(rw io.Writer, req io.Reader) command.Error { return nil } +// SignCredential adds proof to given verifiable credential +func (o *Command) SignCredential(rw io.Writer, req io.Reader) command.Error { + request := &SignCredentialRequest{} + + err := json.NewDecoder(req).Decode(&request) + if err != nil { + logutil.LogInfo(logger, commandName, signCredentialCommandMethod, "request decode : "+err.Error()) + + return command.NewValidationError(InvalidRequestErrorCode, fmt.Errorf("request decode : %w", err)) + } + + didDoc, err := o.ctx.VDRIRegistry().Resolve(request.DID) + // if did not found in VDRI, look through in local storage + if err != nil { + didDoc, err = o.didStore.GetDID(request.DID) + if err != nil { + logutil.LogError(logger, commandName, signCredentialCommandMethod, + "failed to get did doc from store or vdri: "+err.Error()) + + return command.NewValidationError(SignCredentialErrorCode, + fmt.Errorf("generate vp - failed to get did doc from store or vdri : %w", err)) + } + } + + vc, err := verifiable.ParseUnverifiedCredential(request.Credential) + if err != nil { + logutil.LogError(logger, commandName, signCredentialCommandMethod, "parse credential : "+err.Error()) + + return command.NewValidationError(SignCredentialErrorCode, fmt.Errorf("parse vc : %w", err)) + } + + err = o.addCredentialProof(vc, didDoc, request.ProofOptions) + if err != nil { + logutil.LogError(logger, commandName, signCredentialCommandMethod, "sign credential : "+err.Error()) + + return command.NewValidationError(SignCredentialErrorCode, fmt.Errorf("sign credential : %w", err)) + } + + vcBytes, err := vc.MarshalJSON() + if err != nil { + logutil.LogError(logger, commandName, signCredentialCommandMethod, "marshal credential : "+err.Error()) + + return command.NewValidationError(SignCredentialErrorCode, fmt.Errorf("marshal credential : %w", err)) + } + + command.WriteNillableResponse(rw, &SignCredentialResponse{ + VerifiableCredential: vcBytes, + }, logger) + + logutil.LogDebug(logger, commandName, signCredentialCommandMethod, "success") + + return nil +} + // GetPresentation retrieves the verifiable presentation from the store. func (o *Command) GetPresentation(rw io.Writer, req io.Reader) command.Error { var request IDArg @@ -589,7 +653,7 @@ func (o *Command) createAndSignPresentation(credentials []interface{}, vp *verif vp.Holder = holder // Add proofs to vp - sign presentation - vp, err = o.addLinkedDataProof(vp, opts) + err = o.addLinkedDataProof(vp, opts) if err != nil { return nil, fmt.Errorf("failed to sign vp: %w", err) } @@ -610,7 +674,7 @@ func (o *Command) createAndSignPresentationByID(vc *verifiable.Credential, return nil, fmt.Errorf("failed to create vp by ID: %w", err) } - vp, err = o.addLinkedDataProof(vp, &ProofOptions{VerificationMethod: pk, SignatureType: signatureType}) + err = o.addLinkedDataProof(vp, &ProofOptions{VerificationMethod: pk, SignatureType: signatureType}) if err != nil { return nil, fmt.Errorf("failed to sign vp by ID: %w", err) } @@ -618,11 +682,10 @@ func (o *Command) createAndSignPresentationByID(vc *verifiable.Credential, return vp.MarshalJSON() } -func (o *Command) addLinkedDataProof(vp *verifiable.Presentation, opts *ProofOptions) (*verifiable.Presentation, - error) { +func (o *Command) addLinkedDataProof(p provable, opts *ProofOptions) error { s, err := newKMSSigner(o.ctx.KMS(), o.ctx.Crypto(), opts.VerificationMethod) if err != nil { - return nil, err + return err } var signatureSuite verifiablesigner.SignatureSuite @@ -633,7 +696,7 @@ func (o *Command) addLinkedDataProof(vp *verifiable.Presentation, opts *ProofOpt case JSONWebSignature2020: signatureSuite = jsonwebsignature2020.New(suite.WithSigner(s)) default: - return nil, fmt.Errorf("signature type unsupported %s", opts.SignatureType) + return fmt.Errorf("signature type unsupported %s", opts.SignatureType) } signingCtx := &verifiable.LinkedDataProofContext{ @@ -647,12 +710,12 @@ func (o *Command) addLinkedDataProof(vp *verifiable.Presentation, opts *ProofOpt Purpose: opts.proofPurpose, } - err = vp.AddLinkedDataProof(signingCtx) + err = p.AddLinkedDataProof(signingCtx) if err != nil { - return nil, fmt.Errorf("failed to add linked data proof: %w", err) + return fmt.Errorf("failed to add linked data proof: %w", err) } - return vp, nil + return nil } func (o *Command) parsePresentationRequest(request *PresentationRequest, @@ -700,7 +763,7 @@ func (o *Command) parsePresentationRequest(request *PresentationRequest, } } - opts, err := prepareOpts(request.ProofOptions, didDoc) + opts, err := prepareOpts(request.ProofOptions, didDoc, did.Authentication) if err != nil { logutil.LogError(logger, commandName, generatePresentationCommandMethod, "failed to prepare proof options: "+err.Error()) @@ -710,20 +773,25 @@ func (o *Command) parsePresentationRequest(request *PresentationRequest, return vcs, presentation, opts, nil } -func prepareOpts(opts *ProofOptions, didDoc *did.Doc) (*ProofOptions, error) { +func prepareOpts(opts *ProofOptions, didDoc *did.Doc, method did.VerificationRelationship) (*ProofOptions, error) { if opts == nil { opts = &ProofOptions{} } - opts.proofPurpose = "authentication" + var err error + + opts.proofPurpose, err = getProofPurpose(method) + if err != nil { + return nil, err + } - authVMs := didDoc.VerificationMethods(did.Authentication)[did.Authentication] + vMs := didDoc.VerificationMethods(method)[method] vmMatched := opts.VerificationMethod == "" - for _, vm := range authVMs { + for _, vm := range vMs { if opts.VerificationMethod != "" { - // if verification method is provided as an option, then validate if it belongs to 'authentication' + // if verification method is provided as an option, then validate if it belongs to given method if opts.VerificationMethod == vm.PublicKey.ID { vmMatched = true break @@ -738,13 +806,13 @@ func prepareOpts(opts *ProofOptions, didDoc *did.Doc) (*ProofOptions, error) { } if !vmMatched { - return nil, fmt.Errorf("unable to find matching 'authentication' key IDs for given verification method") + return nil, fmt.Errorf("unable to find matching '%s' key IDs for given verification method", opts.proofPurpose) } // this is the fallback logic kept for DIDs not having authentication method // TODO to be removed [Issue #1693] if opts.VerificationMethod == "" { - logger.Warnf("Could not find matching verification method for 'authentication' proof purpose") + logger.Warnf("Could not find matching verification method for '%s' proof purpose", opts.proofPurpose) defaultVM, err := getDefaultVerificationMethod(didDoc) if err != nil { @@ -788,6 +856,29 @@ func getDefaultVerificationMethod(didDoc *did.Doc) (string, error) { } } +func (o *Command) addCredentialProof(vc *verifiable.Credential, didDoc *did.Doc, opts *ProofOptions) error { + var err error + + opts, err = prepareOpts(opts, didDoc, did.AssertionMethod) + if err != nil { + return err + } + + return o.addLinkedDataProof(vc, opts) +} + func isDID(str string) bool { return strings.HasPrefix(str, "did:") } + +func getProofPurpose(method did.VerificationRelationship) (string, error) { + if method != did.Authentication && method != did.AssertionMethod { + return "", fmt.Errorf("unsupported proof purpose, only authentication or assertionMethod are supported") + } + + if method == did.Authentication { + return "authentication", nil + } + + return "assertionMethod", nil +} diff --git a/pkg/controller/command/verifiable/command_test.go b/pkg/controller/command/verifiable/command_test.go index 07f82fda3..78fd44d65 100644 --- a/pkg/controller/command/verifiable/command_test.go +++ b/pkg/controller/command/verifiable/command_test.go @@ -110,6 +110,7 @@ const doc = `{ "@context": ["https://w3id.org/did/v1","https://w3id.org/did/v2"], "id": "did:peer:123456789abcdefghi#inbox", "authentication": ["did:peer:123456789abcdefghi#keys-1"], + "assertionMethod": ["did:peer:123456789abcdefghi#keys-1"], "publicKey": [ { "id": "did:peer:123456789abcdefghi#keys-1", @@ -146,6 +147,7 @@ const jwsDIDDoc = `{ "@context":["https://w3id.org/did/v1"], "id": "did:trustbloc:testnet.trustbloc.local:EiBug_0h2oNJj4Vhk7yrC36HvskhngqTJC46VKS-FDM5fA", "authentication" : [ "did:trustbloc:testnet.trustbloc.local:EiBug_0h2oNJj4Vhk7yrC36HvskhngqTJC46VKS-FDM5fA#key-7777" ], + "assertionMethod" : [ "did:trustbloc:testnet.trustbloc.local:EiBug_0h2oNJj4Vhk7yrC36HvskhngqTJC46VKS-FDM5fA#key-7777" ], "publicKey": [{ "controller": "did:trustbloc:testnet.trustbloc.local:EiBug_0h2oNJj4Vhk7yrC36HvskhngqTJC46VKS-FDM5fA", "id": "did:trustbloc:testnet.trustbloc.local:EiBug_0h2oNJj4Vhk7yrC36HvskhngqTJC46VKS-FDM5fA#key-7777", @@ -315,7 +317,7 @@ func TestNew(t *testing.T) { require.NoError(t, err) handlers := cmd.GetHandlers() - require.Equal(t, 10, len(handlers)) + require.Equal(t, 11, len(handlers)) }) t.Run("test new command - vc store error", func(t *testing.T) { @@ -1592,168 +1594,480 @@ func TestGeneratePresentation_prepareOpts(t *testing.T) { require.Len(t, didStore, len(dids)) - //nolint:lll - t.Run("Test generate presentation opts", func(t *testing.T) { - tests := []struct { - name string - requestDID string - requestOpts *ProofOptions - responseOpts *ProofOptions - err string - }{ - { - name: "with default opts", - requestDID: "did:peer:123456789abcdefghi#inbox", - requestOpts: nil, - responseOpts: &ProofOptions{ - VerificationMethod: "did:peer:123456789abcdefghi#keys-1", - proofPurpose: "authentication", + testPrepareOpts := func(method did.VerificationRelationship) { + //nolint:lll + t.Run("Test generate presentation opts", func(t *testing.T) { + tests := []struct { + name string + requestDID string + requestOpts *ProofOptions + responseOpts *ProofOptions + err string + }{ + { + name: "with default opts", + requestDID: "did:peer:123456789abcdefghi#inbox", + requestOpts: nil, + responseOpts: &ProofOptions{ + VerificationMethod: "did:peer:123456789abcdefghi#keys-1", + proofPurpose: "authentication", + }, }, - }, - { - name: "with default opts when there are two authentication methods in DID", - requestDID: "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd", - requestOpts: &ProofOptions{ - VerificationMethod: "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd#z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd", + { + name: "with default opts when there are two authentication methods in DID", + requestDID: "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd", + requestOpts: &ProofOptions{ + VerificationMethod: "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd#z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd", + }, + responseOpts: &ProofOptions{ + VerificationMethod: "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd#z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd", + proofPurpose: "authentication", + }, }, - responseOpts: &ProofOptions{ - VerificationMethod: "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd#z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd", - proofPurpose: "authentication", + { + name: "second under authentication as verification method", + requestDID: "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd", + requestOpts: &ProofOptions{ + VerificationMethod: "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd#XiRjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd", + }, + responseOpts: &ProofOptions{ + VerificationMethod: "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd#XiRjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd", + proofPurpose: "authentication", + }, }, - }, - { - name: "second under authentication as verification method", - requestDID: "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd", - requestOpts: &ProofOptions{ - VerificationMethod: "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd#XiRjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd", + { + name: "first under authentication as verification method", + requestDID: "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd", + requestOpts: &ProofOptions{ + VerificationMethod: "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd#z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd", + }, + responseOpts: &ProofOptions{ + VerificationMethod: "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd#z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd", + proofPurpose: "authentication", + }, }, - responseOpts: &ProofOptions{ - VerificationMethod: "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd#XiRjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd", - proofPurpose: "authentication", + { + name: "first with only one authentication method", + requestDID: "did:trustbloc:testnet.trustbloc.local:EiAzdTbGPXhvC0ESOcnlR7nCWkN1m1XUJ04uEG9ayhRbPg", + requestOpts: &ProofOptions{ + VerificationMethod: "did:trustbloc:testnet.trustbloc.local:EiAzdTbGPXhvC0ESOcnlR7nCWkN1m1XUJ04uEG9ayhRbPg#bG9jYWwtbG9jazovL2RlZmF1bHQvbWFzdGVyL2tleS96cThTc3JJZ0JVTHhveU9XU2tLZ2drQWJhcjJhVDVHTmlXbERuY244VlYwPQ", + }, + responseOpts: &ProofOptions{ + VerificationMethod: "did:trustbloc:testnet.trustbloc.local:EiAzdTbGPXhvC0ESOcnlR7nCWkN1m1XUJ04uEG9ayhRbPg#bG9jYWwtbG9jazovL2RlZmF1bHQvbWFzdGVyL2tleS96cThTc3JJZ0JVTHhveU9XU2tLZ2drQWJhcjJhVDVHTmlXbERuY244VlYwPQ", + proofPurpose: "authentication", + }, }, - }, - { - name: "first under authentication as verification method", - requestDID: "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd", - requestOpts: &ProofOptions{ - VerificationMethod: "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd#z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd", + { + name: "did without authentication method but VM in opts", + requestDID: "did:trustbloc:testnet.trustbloc.local:EiAzdTbGPXhvC0ESOcnlR7nCWkN1m1XUJ04uEG9ayhRbPg1", + requestOpts: &ProofOptions{ + VerificationMethod: "did:trustbloc:testnet.trustbloc.local:EiAzdTbGPXhvC0ESOcnlR7nCWkN1m1XUJ04uEG9ayhRbPg1#bG9jYWwtbG9jazovL2RlZmF1bHQvbWFzdGVyL2tleS96cThTc3JJZ0JVTHhveU9XU2tLZ2drQWJhcjJhVDVHTmlXbERuY244VlYwPQ", + }, + err: "unable to find matching 'authentication' key IDs for given verification method", }, - responseOpts: &ProofOptions{ - VerificationMethod: "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd#z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd", - proofPurpose: "authentication", + { + name: "did without authentication method but no VM in opts", + requestDID: "did:trustbloc:testnet.trustbloc.local:EiAzdTbGPXhvC0ESOcnlR7nCWkN1m1XUJ04uEG9ayhRbPg1", + requestOpts: &ProofOptions{}, + responseOpts: &ProofOptions{ + VerificationMethod: "did:trustbloc:testnet.trustbloc.local:EiAzdTbGPXhvC0ESOcnlR7nCWkN1m1XUJ04uEG9ayhRbPg1#bG9jYWwtbG9jazovL2RlZmF1bHQvbWFzdGVyL2tleS96cThTc3JJZ0JVTHhveU9XU2tLZ2drQWJhcjJhVDVHTmlXbERuY244VlYwPQ", + proofPurpose: "authentication", + }, }, - }, - { - name: "first with only one authentication method", - requestDID: "did:trustbloc:testnet.trustbloc.local:EiAzdTbGPXhvC0ESOcnlR7nCWkN1m1XUJ04uEG9ayhRbPg", - requestOpts: &ProofOptions{ - VerificationMethod: "did:trustbloc:testnet.trustbloc.local:EiAzdTbGPXhvC0ESOcnlR7nCWkN1m1XUJ04uEG9ayhRbPg#bG9jYWwtbG9jazovL2RlZmF1bHQvbWFzdGVyL2tleS96cThTc3JJZ0JVTHhveU9XU2tLZ2drQWJhcjJhVDVHTmlXbERuY244VlYwPQ", + { + name: "using invalid DID and default opts", + requestDID: "did:peer:21tDAKCERh95uGgKbJNHYp", + requestOpts: &ProofOptions{}, + err: "failed to get default verification method: public key not found in DID Document", }, - responseOpts: &ProofOptions{ - VerificationMethod: "did:trustbloc:testnet.trustbloc.local:EiAzdTbGPXhvC0ESOcnlR7nCWkN1m1XUJ04uEG9ayhRbPg#bG9jYWwtbG9jazovL2RlZmF1bHQvbWFzdGVyL2tleS96cThTc3JJZ0JVTHhveU9XU2tLZ2drQWJhcjJhVDVHTmlXbERuY244VlYwPQ", - proofPurpose: "authentication", + { + name: "using invalid DID and verification method", + requestDID: "did:peer:21tDAKCERh95uGgKbJNHYp", + requestOpts: &ProofOptions{ + VerificationMethod: "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd#z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd", + }, + err: "unable to find matching 'authentication' key IDs for given verification method", }, - }, - { - name: "did without authentication method but VM in opts", - requestDID: "did:trustbloc:testnet.trustbloc.local:EiAzdTbGPXhvC0ESOcnlR7nCWkN1m1XUJ04uEG9ayhRbPg1", - requestOpts: &ProofOptions{ - VerificationMethod: "did:trustbloc:testnet.trustbloc.local:EiAzdTbGPXhvC0ESOcnlR7nCWkN1m1XUJ04uEG9ayhRbPg1#bG9jYWwtbG9jazovL2RlZmF1bHQvbWFzdGVyL2tleS96cThTc3JJZ0JVTHhveU9XU2tLZ2drQWJhcjJhVDVHTmlXbERuY244VlYwPQ", - }, - err: "unable to find matching 'authentication' key IDs for given verification method", - }, - { - name: "did without authentication method but no VM in opts", - requestDID: "did:trustbloc:testnet.trustbloc.local:EiAzdTbGPXhvC0ESOcnlR7nCWkN1m1XUJ04uEG9ayhRbPg1", - requestOpts: &ProofOptions{}, - responseOpts: &ProofOptions{ - VerificationMethod: "did:trustbloc:testnet.trustbloc.local:EiAzdTbGPXhvC0ESOcnlR7nCWkN1m1XUJ04uEG9ayhRbPg1#bG9jYWwtbG9jazovL2RlZmF1bHQvbWFzdGVyL2tleS96cThTc3JJZ0JVTHhveU9XU2tLZ2drQWJhcjJhVDVHTmlXbERuY244VlYwPQ", - proofPurpose: "authentication", + { + name: "private key matching second verification method", + requestDID: "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd", + requestOpts: &ProofOptions{ + VerificationMethod: "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd#XiRjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd", + }, + responseOpts: &ProofOptions{ + VerificationMethod: "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd#XiRjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd", + proofPurpose: "authentication", + }, }, - }, - { - name: "using invalid DID and default opts", - requestDID: "did:peer:21tDAKCERh95uGgKbJNHYp", - requestOpts: &ProofOptions{}, - err: "failed to get default verification method: public key not found in DID Document", - }, - { - name: "using invalid DID and verification method", - requestDID: "did:peer:21tDAKCERh95uGgKbJNHYp", - requestOpts: &ProofOptions{ - VerificationMethod: "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd#z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd", + { + name: "private key matching first verification method", + requestDID: "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd", + requestOpts: &ProofOptions{ + VerificationMethod: "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd#z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd", + }, + responseOpts: &ProofOptions{ + VerificationMethod: "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd#z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd", + proofPurpose: "authentication", + }, }, - err: "unable to find matching 'authentication' key IDs for given verification method", - }, - { - name: "private key matching second verification method", - requestDID: "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd", - requestOpts: &ProofOptions{ - VerificationMethod: "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd#XiRjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd", + { + name: "private key not matching any verification method", + requestDID: "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd", + requestOpts: &ProofOptions{ + VerificationMethod: "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd#XiRjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHdXYZ", + }, + err: "unable to find matching 'authentication' key IDs for given verification method", }, - responseOpts: &ProofOptions{ - VerificationMethod: "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd#XiRjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd", - proofPurpose: "authentication", + { + name: "all opts given", + requestDID: "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd", + requestOpts: &ProofOptions{ + VerificationMethod: "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd#XiRjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd", + Domain: "sample.domain.example.com", + Challenge: "sample-challenge", + SignatureType: JSONWebSignature2020, + }, + responseOpts: &ProofOptions{ + VerificationMethod: "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd#XiRjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd", + proofPurpose: "authentication", + Domain: "sample.domain.example.com", + Challenge: "sample-challenge", + SignatureType: JSONWebSignature2020, + }, }, + } + + t.Parallel() + + for _, test := range tests { + tc := test + t.Run(tc.name, func(t *testing.T) { + res, err := prepareOpts(tc.requestOpts, didStore[tc.requestDID], method) + + if tc.err != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tc.err) + require.Nil(t, res) + + return + } + + require.NoError(t, err) + require.NotNil(t, res) + require.Equal(t, tc.responseOpts, res) + }) + } + }) + } + + testPrepareOpts(did.Authentication) +} + +func TestCommand_SignCredential(t *testing.T) { + s := make(map[string][]byte) + cmd, cmdErr := New(&mockprovider.Provider{ + StorageProviderValue: &mockstore.MockStoreProvider{Store: &mockstore.MockStore{Store: s}}, + VDRIRegistryValue: &mockvdri.MockVDRIRegistry{ + ResolveFunc: func(didID string, opts ...vdri.ResolveOpts) (*did.Doc, error) { + if didID == invalidDID { + return nil, errors.New("invalid") + } + + if didID == jwsDID { + jwsDoc, err := did.ParseDocument([]byte(jwsDIDDoc)) + if err != nil { + return nil, errors.New("unmarshal failed ") + } + return jwsDoc, nil + } + + didDoc, err := did.ParseDocument([]byte(doc)) + if err != nil { + return nil, errors.New("unmarshal failed ") + } + return didDoc, nil }, - { - name: "private key matching first verification method", - requestDID: "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd", - requestOpts: &ProofOptions{ - VerificationMethod: "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd#z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd", - }, - responseOpts: &ProofOptions{ - VerificationMethod: "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd#z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd", - proofPurpose: "authentication", - }, + }, + KMSValue: &kmsmock.KeyManager{}, + CryptoValue: &cryptomock.Crypto{}, + }) + + require.NotNil(t, cmd) + require.NoError(t, cmdErr) + + t.Run("test sign credential - success", func(t *testing.T) { + req := SignCredentialRequest{ + Credential: []byte(vc), + DID: "did:peer:123456789abcdefghi#inbox", + ProofOptions: &ProofOptions{SignatureType: Ed25519Signature2018}, + } + reqBytes, err := json.Marshal(req) + require.NoError(t, err) + + var b bytes.Buffer + err = cmd.SignCredential(&b, bytes.NewBuffer(reqBytes)) + require.NoError(t, err) + + // verify response + var response SignCredentialResponse + err = json.NewDecoder(&b).Decode(&response) + require.NoError(t, err) + + require.NotEmpty(t, response) + }) + + t.Run("test sign credential - jws key - success", func(t *testing.T) { + req := SignCredentialRequest{ + Credential: []byte(vc), + DID: jwsDID, + ProofOptions: &ProofOptions{SignatureType: Ed25519Signature2018}, + } + reqBytes, err := json.Marshal(req) + require.NoError(t, err) + + var b bytes.Buffer + err = cmd.SignCredential(&b, bytes.NewBuffer(reqBytes)) + require.NoError(t, err) + + // verify response + var response SignCredentialResponse + err = json.NewDecoder(&b).Decode(&response) + require.NoError(t, err) + + require.NotEmpty(t, response) + require.Contains(t, string(response.VerifiableCredential), + "did:trustbloc:testnet.trustbloc.local:EiBug_0h2oNJj4Vhk7yrC36HvskhngqTJC46VKS-FDM5fA#key-7777") + }) + + t.Run("test sign credential with proof options - success", func(t *testing.T) { + createdTime := time.Now().AddDate(-1, 0, 0) + req := SignCredentialRequest{ + Credential: []byte(vc), + DID: "did:peer:123456789abcdefghi#inbox", + ProofOptions: &ProofOptions{ + VerificationMethod: "did:peer:123456789abcdefghi#keys-1", + Domain: "issuer.example.com", + Challenge: "sample-random-test-value", + Created: &createdTime, + SignatureType: Ed25519Signature2018, }, - { - name: "private key not matching any verification method", - requestDID: "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd", - requestOpts: &ProofOptions{ - VerificationMethod: "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd#XiRjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHdXYZ", - }, - err: "unable to find matching 'authentication' key IDs for given verification method", + } + + reqBytes, err := json.Marshal(req) + require.NoError(t, err) + + var b bytes.Buffer + err = cmd.SignCredential(&b, bytes.NewBuffer(reqBytes)) + require.NoError(t, err) + + // verify response + var response SignCredentialResponse + err = json.NewDecoder(&b).Decode(&response) + require.NoError(t, err) + require.NotEmpty(t, response) + + vc, err := verifiable.ParseCredential(response.VerifiableCredential, verifiable.WithDisabledProofCheck()) + + require.NoError(t, err) + require.NotNil(t, vc) + require.NotEmpty(t, vc.Proofs) + require.Len(t, vc.Proofs, 1) + require.Equal(t, vc.Proofs[0]["challenge"], req.Challenge) + require.Equal(t, vc.Proofs[0]["domain"], req.Domain) + require.Equal(t, vc.Proofs[0]["proofPurpose"], "assertionMethod") + require.Contains(t, vc.Proofs[0]["created"], strconv.Itoa(req.Created.Year())) + }) + + t.Run("test sign credential with proof options with default vm - success", func(t *testing.T) { + createdTime := time.Now().AddDate(-1, 0, 0) + req := SignCredentialRequest{ + Credential: []byte(vc), + DID: "did:peer:123456789abcdefghi#inbox", + ProofOptions: &ProofOptions{ + Domain: "issuer.example.com", + Challenge: "sample-random-test-value", + Created: &createdTime, + SignatureType: Ed25519Signature2018, }, - { - name: "all opts given", - requestDID: "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd", - requestOpts: &ProofOptions{ - VerificationMethod: "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd#XiRjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd", - Domain: "sample.domain.example.com", - Challenge: "sample-challenge", - SignatureType: JSONWebSignature2020, - }, - responseOpts: &ProofOptions{ - VerificationMethod: "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd#XiRjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd", - proofPurpose: "authentication", - Domain: "sample.domain.example.com", - Challenge: "sample-challenge", - SignatureType: JSONWebSignature2020, - }, + } + + reqBytes, err := json.Marshal(req) + require.NoError(t, err) + + var b bytes.Buffer + err = cmd.SignCredential(&b, bytes.NewBuffer(reqBytes)) + require.NoError(t, err) + + // verify response + var response SignCredentialResponse + err = json.NewDecoder(&b).Decode(&response) + require.NoError(t, err) + require.NotEmpty(t, response) + + vp, err := verifiable.ParseCredential(response.VerifiableCredential, verifiable.WithDisabledProofCheck()) + + require.NoError(t, err) + require.NotNil(t, vp) + require.NotEmpty(t, vp.Proofs) + require.Len(t, vp.Proofs, 1) + require.Equal(t, vp.Proofs[0]["challenge"], req.Challenge) + require.Equal(t, vp.Proofs[0]["domain"], req.Domain) + require.Equal(t, vp.Proofs[0]["proofPurpose"], "assertionMethod") + require.Contains(t, vp.Proofs[0]["created"], strconv.Itoa(req.Created.Year())) + require.Equal(t, vp.Proofs[0]["verificationMethod"], + "did:peer:123456789abcdefghi#keys-1") + }) + + t.Run("test sign credential with proof options - success (p256 jsonwebsignature)", func(t *testing.T) { + createdTime := time.Now().AddDate(-1, 0, 0) + req := SignCredentialRequest{ + Credential: []byte(vc), + DID: "did:peer:123456789abcdefghi#inbox", + ProofOptions: &ProofOptions{ + Domain: "issuer.example.com", + Challenge: "sample-random-test-value", + Created: &createdTime, + VerificationMethod: "did:peer:123456789abcdefghi#keys-1", + SignatureType: JSONWebSignature2020, }, } - t.Parallel() + reqBytes, err := json.Marshal(req) + require.NoError(t, err) - for _, test := range tests { - tc := test - t.Run(tc.name, func(t *testing.T) { - res, err := prepareOpts(tc.requestOpts, didStore[tc.requestDID]) + var b bytes.Buffer + err = cmd.SignCredential(&b, bytes.NewBuffer(reqBytes)) + require.NoError(t, err) - if tc.err != "" { - require.Error(t, err) - require.Contains(t, err.Error(), tc.err) - require.Nil(t, res) + // verify response + var response SignCredentialResponse + err = json.NewDecoder(&b).Decode(&response) + require.NoError(t, err) + require.NotEmpty(t, response) - return - } + vc, err := verifiable.ParseCredential(response.VerifiableCredential, verifiable.WithDisabledProofCheck()) - require.NoError(t, err) - require.NotNil(t, res) - require.Equal(t, tc.responseOpts, res) - }) + require.NoError(t, err) + require.NotNil(t, vc) + require.NotEmpty(t, vc.Proofs) + require.Len(t, vc.Proofs, 1) + require.Equal(t, vc.Proofs[0]["challenge"], req.Challenge) + require.Equal(t, vc.Proofs[0]["domain"], req.Domain) + require.Equal(t, vc.Proofs[0]["proofPurpose"], "assertionMethod") + require.Contains(t, vc.Proofs[0]["created"], strconv.Itoa(req.Created.Year())) + require.Contains(t, vc.Proofs[0]["type"], "JsonWebSignature2020") + }) + + t.Run("test sign credential with proof options - success (ed25519 jsonwebsignature)", func(t *testing.T) { + createdTime := time.Now().AddDate(-1, 0, 0) + req := SignCredentialRequest{ + Credential: []byte(vc), + DID: jwsDID, + ProofOptions: &ProofOptions{ + VerificationMethod: "did:trustbloc:testnet.trustbloc.local:EiBug_0h2oNJj4Vhk7yrC36HvskhngqTJC46VKS-FDM5fA#key-7777", + Domain: "issuer.example.com", + Challenge: "sample-random-test-value", + Created: &createdTime, + SignatureType: JSONWebSignature2020, + }, } + + reqBytes, err := json.Marshal(req) + require.NoError(t, err) + + var b bytes.Buffer + err = cmd.SignCredential(&b, bytes.NewBuffer(reqBytes)) + require.NoError(t, err) + + // verify response + var response SignCredentialResponse + err = json.NewDecoder(&b).Decode(&response) + require.NoError(t, err) + require.NotEmpty(t, response) + + vc, err := verifiable.ParseCredential(response.VerifiableCredential, verifiable.WithDisabledProofCheck()) + + require.NoError(t, err) + require.NotNil(t, vc) + require.NotEmpty(t, vc.Proofs) + require.Len(t, vc.Proofs, 1) + require.Equal(t, vc.Proofs[0]["challenge"], req.Challenge) + require.Equal(t, vc.Proofs[0]["domain"], req.Domain) + require.Equal(t, vc.Proofs[0]["proofPurpose"], "assertionMethod") + require.Contains(t, vc.Proofs[0]["created"], strconv.Itoa(req.Created.Year())) + require.Contains(t, vc.Proofs[0]["type"], "JsonWebSignature2020") + }) + + t.Run("test sign credential with proof options - unsupported signature type", func(t *testing.T) { + req := SignCredentialRequest{ + Credential: []byte(vc), + DID: "did:peer:123456789abcdefghi#inbox", + ProofOptions: &ProofOptions{ + VerificationMethod: "did:peer:123456789abcdefghi#keys-1", + SignatureType: "invalid", + }, + } + + reqBytes, err := json.Marshal(req) + require.NoError(t, err) + + var b bytes.Buffer + err = cmd.SignCredential(&b, bytes.NewBuffer(reqBytes)) + require.Error(t, err) + require.Contains(t, err.Error(), "signature type unsupported invalid") + }) + + t.Run("test sign credential with proof options - signature type empty", func(t *testing.T) { + req := SignCredentialRequest{ + Credential: []byte(vc), + DID: "did:peer:123456789abcdefghi#inbox", + ProofOptions: &ProofOptions{Domain: "domain"}, + } + + presReqBytes, err := json.Marshal(req) + require.NoError(t, err) + + var b bytes.Buffer + err = cmd.SignCredential(&b, bytes.NewBuffer(presReqBytes)) + require.Error(t, err) + require.Contains(t, err.Error(), "signature type unsupported") + }) + + t.Run("test sign credential - invalid request", func(t *testing.T) { + var b bytes.Buffer + + err := cmd.SignCredential(&b, bytes.NewBufferString("--")) + require.Error(t, err) + require.Contains(t, err.Error(), "request decode") + }) + + t.Run("test sign credential - validation error", func(t *testing.T) { + presReq := SignCredentialRequest{ + Credential: []byte("{}"), + DID: "did:peer:123456789abcdefghi#inbox", + ProofOptions: &ProofOptions{SignatureType: Ed25519Signature2018}} + reqBytes, err := json.Marshal(presReq) + require.NoError(t, err) + + var b bytes.Buffer + + err = cmd.SignCredential(&b, bytes.NewBuffer(reqBytes)) + require.Error(t, err) + require.Contains(t, err.Error(), "parse vc : build new credential: fill credential types from raw:") + }) + + t.Run("test sign credential - failed to sign credential", func(t *testing.T) { + presReq := SignCredentialRequest{ + Credential: []byte(vc), + DID: "did:error:123"} + presReqBytes, err := json.Marshal(presReq) + require.NoError(t, err) + + var b bytes.Buffer + + err = cmd.SignCredential(&b, bytes.NewBuffer(presReqBytes)) + require.Error(t, err) + require.Contains(t, err.Error(), "generate vp - failed to get did doc from store or vdri") }) } diff --git a/pkg/controller/command/verifiable/models.go b/pkg/controller/command/verifiable/models.go index 83bdeb3da..1a107aac7 100644 --- a/pkg/controller/command/verifiable/models.go +++ b/pkg/controller/command/verifiable/models.go @@ -59,6 +59,18 @@ type CredentialExt struct { Name string `json:"name,omitempty"` } +// SignCredentialRequest is adding proof to given credential. +type SignCredentialRequest struct { + Credential json.RawMessage `json:"credential,omitempty"` + DID string `json:"did,omitempty"` + *ProofOptions +} + +// SignCredentialResponse is model for sign credential response. +type SignCredentialResponse struct { + VerifiableCredential json.RawMessage `json:"verifiableCredential,omitempty"` +} + // PresentationExt is model for presentation with fields related to command features. type PresentationExt struct { Presentation diff --git a/pkg/controller/rest/verifiable/models.go b/pkg/controller/rest/verifiable/models.go index 6ea94e168..fd2c2adef 100644 --- a/pkg/controller/rest/verifiable/models.go +++ b/pkg/controller/rest/verifiable/models.go @@ -140,10 +140,10 @@ type presentationRecordResult struct { // // swagger:parameters generatePresentationReq type generatePresentationReq struct { // nolint: unused,deadcode - // Params for generating the verifiable presentation (pass the vc document as a string) + // Params for generating the verifiable presentation (pass the vc document as a raw JSON) // // in: body - Params verifiable.Credential + Params verifiable.PresentationRequest } // presentationRes model @@ -156,3 +156,26 @@ type presentationRes struct { // in: body VerifiablePresentation json.RawMessage `json:"verifiablePresentation,omitempty"` } + +// signCredentialReq model +// +// This is used to sign a credential. +// +// swagger:parameters signCredentialReq +type signCredentialReq struct { // nolint: unused,deadcode + // Params for signing a credential + // + // in: body + Params verifiable.SignCredentialRequest +} + +// signCredentialRes model +// +// This is used for returning the sign credential response +// +// swagger:response signCredentialRes +type signCredentialRes struct { + + // in: body + VerifiableCredential json.RawMessage `json:"verifiableCredential,omitempty"` +} diff --git a/pkg/controller/rest/verifiable/operation.go b/pkg/controller/rest/verifiable/operation.go index 22cca6eee..9ccb828af 100644 --- a/pkg/controller/rest/verifiable/operation.go +++ b/pkg/controller/rest/verifiable/operation.go @@ -35,6 +35,7 @@ const ( getCredentialPath = verifiableCredentialPath + "/{id}" getCredentialByNamePath = verifiableCredentialPath + "/name" + "/{name}" getCredentialsPath = verifiableOperationID + "/credentials" + signCredentialsPath = verifiableOperationID + "/signcredential" // presentation paths generatePresentationPath = verifiablePresentationPath + "/generate" @@ -84,6 +85,7 @@ func (o *Operation) registerHandler() { cmdutil.NewHTTPHandler(getCredentialPath, http.MethodGet, o.GetCredential), cmdutil.NewHTTPHandler(getCredentialByNamePath, http.MethodGet, o.GetCredentialByName), cmdutil.NewHTTPHandler(getCredentialsPath, http.MethodGet, o.GetCredentials), + cmdutil.NewHTTPHandler(signCredentialsPath, http.MethodPost, o.SignCredential), cmdutil.NewHTTPHandler(generatePresentationPath, http.MethodPost, o.GeneratePresentation), cmdutil.NewHTTPHandler(generatePresentationByIDPath, http.MethodPost, o.GeneratePresentationByID), cmdutil.NewHTTPHandler(savePresentationPath, http.MethodPost, o.SavePresentation), @@ -193,6 +195,17 @@ func (o *Operation) GetCredentials(rw http.ResponseWriter, req *http.Request) { rest.Execute(o.command.GetCredentials, rw, req.Body) } +// SignCredential swagger:route POST /verifiable/signcredential verifiable signCredentialReq +// +// Signs given credential. +// +// Responses: +// default: genericError +// 200: signCredentialRes +func (o *Operation) SignCredential(rw http.ResponseWriter, req *http.Request) { + rest.Execute(o.command.SignCredential, rw, req.Body) +} + // GetPresentations swagger:route GET /verifiable/presentations verifiable // // Retrieves the verifiable credentials. diff --git a/pkg/controller/rest/verifiable/operation_test.go b/pkg/controller/rest/verifiable/operation_test.go index 164eaae6b..921819b9d 100644 --- a/pkg/controller/rest/verifiable/operation_test.go +++ b/pkg/controller/rest/verifiable/operation_test.go @@ -39,6 +39,7 @@ const sampleCredentialName = "sampleVCName" const samplePresentationName = "sampleVPName" const sampleVCID = "http://example.edu/credentials/1989" const sampleVPID = "http://example.edu/presentations/1989" +const invalidDID = "did:error:1234" const vc = ` { @@ -97,6 +98,7 @@ const doc = `{ "@context": ["https://w3id.org/did/v1","https://w3id.org/did/v2"], "id": "did:peer:21tDAKCERh95uGgKbJNHYp", "authentication": ["did:peer:123456789abcdefghi#keys-1"], + "assertionMethod": ["did:peer:123456789abcdefghi#keys-1"], "publicKey": [ { "id": "did:peer:123456789abcdefghi#keys-1", @@ -205,7 +207,7 @@ func TestNew(t *testing.T) { }) require.NoError(t, err) require.NotNil(t, cmd) - require.Equal(t, 10, len(cmd.GetRESTHandlers())) + require.Equal(t, 11, len(cmd.GetRESTHandlers())) }) t.Run("test new command - error", func(t *testing.T) { @@ -448,7 +450,6 @@ func TestGetCredentials(t *testing.T) { func TestGeneratePresentation(t *testing.T) { s := make(map[string][]byte) - invalidDID := "did:error:123" cmd, cmdErr := New(&mockprovider.Provider{ StorageProviderValue: &mockstore.MockStoreProvider{Store: &mockstore.MockStore{Store: s}}, VDRIRegistryValue: &mockvdri.MockVDRIRegistry{ @@ -640,7 +641,6 @@ func TestGeneratePresentation(t *testing.T) { func TestGeneratePresentationByID(t *testing.T) { s := make(map[string][]byte) - invalidDID := "did:error:123" cmd, cmdErr := New(&mockprovider.Provider{ StorageProviderValue: &mockstore.MockStoreProvider{Store: &mockstore.MockStore{Store: s}}, VDRIRegistryValue: &mockvdri.MockVDRIRegistry{ @@ -880,6 +880,153 @@ func TestGetPresentations(t *testing.T) { }) } +func TestSignCredential(t *testing.T) { + s := make(map[string][]byte) + cmd, cmdErr := New(&mockprovider.Provider{ + StorageProviderValue: &mockstore.MockStoreProvider{Store: &mockstore.MockStore{Store: s}}, + VDRIRegistryValue: &mockvdri.MockVDRIRegistry{ + ResolveFunc: func(didID string, opts ...vdri.ResolveOpts) (didDoc *did.Doc, e error) { + if didID == invalidDID { + return nil, errors.New("invalid") + } + didDoc, err := did.ParseDocument([]byte(doc)) + if err != nil { + return nil, errors.New("unmarshal failed ") + } + return didDoc, nil + }, + }, + KMSValue: &kmsmock.KeyManager{}, + CryptoValue: &cryptomock.Crypto{}, + }) + + require.NotNil(t, cmd) + require.NoError(t, cmdErr) + + t.Run("test sign credential - success", func(t *testing.T) { + req := verifiable.SignCredentialRequest{ + Credential: []byte(vc), + DID: "did:peer:21tDAKCERh95uGgKbJNHYp", + ProofOptions: &verifiable.ProofOptions{ + SignatureType: verifiable.Ed25519Signature2018, + }, + } + + reqBytes, err := json.Marshal(req) + require.NoError(t, err) + + handler := lookupHandler(t, cmd, signCredentialsPath, http.MethodPost) + buf, err := getSuccessResponseFromHandler(handler, bytes.NewBuffer(reqBytes), handler.Path()) + require.NoError(t, err, err) + + response := signCredentialRes{} + err = json.Unmarshal(buf.Bytes(), &response) + require.NoError(t, err) + + // verify response + require.NotEmpty(t, response) + require.NotEmpty(t, response.VerifiableCredential) + }) + + t.Run("test sign credential with options - success", func(t *testing.T) { + createdTime := time.Now().AddDate(-1, 0, 0) + req := verifiable.SignCredentialRequest{ + Credential: []byte(vc), + DID: "did:peer:21tDAKCERh95uGgKbJNHYp", + ProofOptions: &verifiable.ProofOptions{ + VerificationMethod: "did:peer:123456789abcdefghi#keys-1", + Domain: "issuer.example.com", + Challenge: "sample-random-test-value", + Created: &createdTime, + SignatureType: verifiable.Ed25519Signature2018, + }, + } + reqBytes, err := json.Marshal(req) + require.NoError(t, err) + + handler := lookupHandler(t, cmd, signCredentialsPath, http.MethodPost) + buf, err := getSuccessResponseFromHandler(handler, bytes.NewBuffer(reqBytes), handler.Path()) + require.NoError(t, err) + + response := signCredentialRes{} + err = json.Unmarshal(buf.Bytes(), &response) + require.NoError(t, err) + + // verify response + require.NotEmpty(t, response) + require.NotEmpty(t, response.VerifiableCredential) + + vp, err := verifiableapi.ParseCredential([]byte(response.VerifiableCredential), + verifiableapi.WithDisabledProofCheck()) + + require.NoError(t, err) + require.NotNil(t, vp) + require.NotEmpty(t, vp.Proofs) + require.Len(t, vp.Proofs, 1) + require.Equal(t, vp.Proofs[0]["challenge"], req.Challenge) + require.Equal(t, vp.Proofs[0]["domain"], req.Domain) + require.Equal(t, vp.Proofs[0]["proofPurpose"], "assertionMethod") + require.Contains(t, vp.Proofs[0]["created"], strconv.Itoa(req.Created.Year())) + }) + + t.Run("test sign credential using options - success", func(t *testing.T) { + createdTime := time.Now().AddDate(-1, 0, 0) + req := verifiable.SignCredentialRequest{ + Credential: []byte(vc), + DID: "did:peer:21tDAKCERh95uGgKbJNHYp", + ProofOptions: &verifiable.ProofOptions{ + VerificationMethod: "did:peer:123456789abcdefghi#keys-1", + Domain: "issuer.example.com", + Challenge: "sample-random-test-value", + Created: &createdTime, + SignatureType: verifiable.Ed25519Signature2018, + }, + } + reqBytes, err := json.Marshal(req) + require.NoError(t, err) + + handler := lookupHandler(t, cmd, signCredentialsPath, http.MethodPost) + buf, err := getSuccessResponseFromHandler(handler, bytes.NewBuffer(reqBytes), handler.Path()) + require.NoError(t, err) + + response := signCredentialRes{} + err = json.Unmarshal(buf.Bytes(), &response) + require.NoError(t, err) + + // verify response + require.NotEmpty(t, response) + require.NotEmpty(t, response.VerifiableCredential) + + vc, err := verifiableapi.ParseCredential([]byte(response.VerifiableCredential), + verifiableapi.WithDisabledProofCheck()) + + require.NoError(t, err) + require.NotNil(t, vc) + require.NotEmpty(t, vc.Proofs) + require.Len(t, vc.Proofs, 1) + require.Equal(t, vc.Proofs[0]["challenge"], req.Challenge) + require.Equal(t, vc.Proofs[0]["domain"], req.Domain) + require.Equal(t, vc.Proofs[0]["proofPurpose"], "assertionMethod") + require.Contains(t, vc.Proofs[0]["created"], strconv.Itoa(req.Created.Year())) + }) + + t.Run("test sign credential - error", func(t *testing.T) { + var jsonStr = []byte(`{ + "name" : "sample", + "did" : "did:peer:21tDAKCERh95uGgKbJNHYp" + }`) + + handler := lookupHandler(t, cmd, signCredentialsPath, http.MethodPost) + buf, code, err := sendRequestToHandler(handler, bytes.NewBuffer(jsonStr), handler.Path()) + require.NoError(t, err) + require.NotEmpty(t, buf) + + require.Equal(t, http.StatusBadRequest, code) + verifyError(t, verifiable.SignCredentialErrorCode, + "parse vc : unmarshal new credential", buf.Bytes()) + }) +} + func lookupHandler(t *testing.T, op *Operation, path, method string) rest.Handler { handlers := op.GetRESTHandlers() require.NotEmpty(t, handlers) diff --git a/test/aries-js-worker/test/verifiable/verifiable.js b/test/aries-js-worker/test/verifiable/verifiable.js index ebed88c19..ab85b59e9 100644 --- a/test/aries-js-worker/test/verifiable/verifiable.js +++ b/test/aries-js-worker/test/verifiable/verifiable.js @@ -180,6 +180,21 @@ async function verifiableStore(newAries, mode = wasmMode) { ) }) + it(modePrefix + "Alice generates the signed verifiable credential to pass it to the employer", async function () { + await aries.verifiable.signCredential({ + "credential": JSON.parse(vc), + "did": did.id, + "signatureType": "JsonWebSignature2020" + }).then( + resp => { + try { + assert.isNotEmpty(resp.verifiableCredential.proof) + } catch (err) { + assert.fail(err) + } + }, err => assert.fail(err) + ) + }); it(modePrefix + "Alice generates the signed verifiable presentation to pass it to the employer", async function () { await aries.verifiable.generatePresentation({