From d318db2a2c45df970fe142fbe4de1574ef1c1f17 Mon Sep 17 00:00:00 2001 From: Lyuben Penkovski <lyuben.penkovski@vereign.com> Date: Tue, 31 Oct 2023 11:41:46 +0200 Subject: [PATCH] Unit and integration tests for create VC endpoint --- integration/integration_test.go | 135 +++++++++++++++---- integration/internal/client/signer.go | 19 +++ internal/service/signer/service_test.go | 165 +++++++++++++++++++++++- 3 files changed, 293 insertions(+), 26 deletions(-) diff --git a/integration/integration_test.go b/integration/integration_test.go index 445abcb..e2fe64d 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -5,13 +5,13 @@ import ( "fmt" "net/http" "os" + "strings" "testing" + "github.com/hyperledger/aries-framework-go/pkg/doc/verifiable" "github.com/hyperledger/aries-framework-go/pkg/vdr" "github.com/hyperledger/aries-framework-go/pkg/vdr/key" "github.com/hyperledger/aries-framework-go/pkg/vdr/web" - - "github.com/hyperledger/aries-framework-go/pkg/doc/verifiable" "github.com/piprate/json-gold/ld" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -119,7 +119,7 @@ func TestCreateCredentialProof(t *testing.T) { { name: "credential with invalid subject id", vc: []byte(credentialInvalidSubjectID), - errMsg: "invalid format of subject id", + errMsg: "invalid subject id: must be URI", }, { name: "credential with numerical subject id", @@ -256,7 +256,7 @@ func TestCreatePresentationProof(t *testing.T) { { name: "presentation with credential with invalid subject id", vp: []byte(presentationWithInvalidSubjectID), - errMsg: "invalid format of subject id", + errMsg: "invalid subject id: must be URI", }, { name: "presentation with credential with numerical subject id", @@ -299,6 +299,90 @@ func TestCreatePresentationProof(t *testing.T) { } } +func TestCreateCredential(t *testing.T) { + initTests(t) + + tests := []struct { + name string + req map[string]interface{} + contexts []string + errtext string + }{ + { + name: "empty request", + errtext: "400 Bad Request", + }, + { + name: "invalid request because issuer is missing", + req: map[string]interface{}{ + "namespace": "transit", + "key": "key1", + "credentialSubject": map[string]interface{}{"cred1": "value1"}, + }, + errtext: "400 Bad Request", + }, + { + name: "valid request with single credentialSubject claim", + req: map[string]interface{}{ + "issuer": "did:web:comic-bullfrog-smoothly.ngrok-free.app:policy:example:returnDID:1.0:evaluation", + "namespace": "transit", + "key": "key1", + "credentialSubject": map[string]interface{}{"cred1": "value1"}, + }, + }, + { + name: "valid request with multiple credentialSubject claims", + req: map[string]interface{}{ + "issuer": "did:web:comic-bullfrog-smoothly.ngrok-free.app:policy:example:returnDID:1.0:evaluation", + "namespace": "transit", + "key": "key1", + "credentialSubject": map[string]interface{}{ + "cred1": "value1", + "cred2": "value2", + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + reqData, err := json.Marshal(test.req) + require.NoError(t, err) + + signer := client.NewSigner(addr) + vcWithProof, err := signer.CreateCredential(reqData) + if test.errtext != "" { + require.Error(t, err, fmt.Sprintf("got no error but expected %q", test.errtext)) + assert.Contains(t, err.Error(), test.errtext) + return + } + + vc, err := verifyCredentialProofs(vcWithProof) + require.NoError(t, err) + assert.NotNil(t, vc) + + assert.NotEmpty(t, vc.Proofs) + assert.NotEmpty(t, vc.Proofs[0]["jws"]) + assert.NotEmpty(t, vc.Proofs[0]["created"]) + assert.NotEmpty(t, vc.Proofs[0]["verificationMethod"]) + assert.Equal(t, "assertionMethod", vc.Proofs[0]["proofPurpose"]) + assert.Equal(t, "JsonWebSignature2020", vc.Proofs[0]["type"]) + + // hyperledger aries always parse the subject map into an array (unless it's just a string) + subject, ok := vc.Subject.([]verifiable.Subject) + assert.True(t, ok) + + expectedClaims, ok := test.req["credentialSubject"].(map[string]interface{}) + assert.True(t, ok) + assert.Equal(t, len(expectedClaims), len(subject[0].CustomFields)) + + for key := range expectedClaims { + assert.Equal(t, expectedClaims[key], subject[0].CustomFields[key]) + } + }) + } +} + func TestCreatePresentation(t *testing.T) { initTests(t) @@ -317,7 +401,9 @@ func TestCreatePresentation(t *testing.T) { req: map[string]interface{}{ "namespace": "transit", "key": "key1", - "data": map[string]interface{}{"cred1": "value1"}, + "data": []map[string]interface{}{ + {"cred1": "value1"}, + }, }, errtext: "400 Bad Request", }, @@ -393,7 +479,7 @@ func TestCreatePresentation(t *testing.T) { creds := vp.Credentials() requiredCreds, ok := test.req["data"].([]map[string]interface{}) assert.True(t, ok) - assert.Equal(t, len(creds), len(requiredCreds)) + assert.Equal(t, len(requiredCreds), len(creds)) for i, cred := range creds { c, ok := cred.(map[string]interface{}) @@ -468,15 +554,9 @@ func TestCreateCredentialMultipleProofs(t *testing.T) { // modify JWS value of the first proof by removing the last character proof1 := parsedVC.Proofs[0] if jws, ok := proof1["jws"].(string); ok && jws != "" { - // substitute last char of JWS with another one, so that the signature - // should become invalid - var modifiedJWS string - if jws[len(jws)-1] != 'a' { - modifiedJWS = jws[:len(jws)-1] + "a" - } else { - modifiedJWS = jws[:len(jws)-1] + "b" - } - parsedVC.Proofs[0]["jws"] = modifiedJWS + modifiedSignature, err := modifySignature(jws) + require.NoError(t, err) + parsedVC.Proofs[0]["jws"] = modifiedSignature } else { t.Errorf("expected to have proof 1 but it's missing or invalid") } @@ -487,7 +567,7 @@ func TestCreateCredentialMultipleProofs(t *testing.T) { assert.NotNil(t, modifiedVC) _, err = verifyCredentialProofs(modifiedVC) - assert.Error(t, err) + require.Error(t, err) assert.Contains(t, err.Error(), "invalid signature") }) @@ -501,15 +581,9 @@ func TestCreateCredentialMultipleProofs(t *testing.T) { // modify JWS value of the second proof by removing the last character proof2 := parsedVC.Proofs[1] if jws, ok := proof2["jws"].(string); ok && jws != "" { - // substitute last char of JWS with another one, so that the signature - // should become invalid - var modifiedJWS string - if jws[len(jws)-1] != 'a' { - modifiedJWS = jws[:len(jws)-1] + "a" - } else { - modifiedJWS = jws[:len(jws)-1] + "b" - } - parsedVC.Proofs[1]["jws"] = modifiedJWS + modifiedSignature, err := modifySignature(jws) + require.NoError(t, err) + parsedVC.Proofs[1]["jws"] = modifiedSignature } else { t.Errorf("expected to have proof 2 but it's missing or invalid") } @@ -546,3 +620,14 @@ func verifyCredentialProofs(vcBytes []byte) (*verifiable.Credential, error) { return vc, err } + +func modifySignature(jws string) (string, error) { + parts := strings.Split(jws, ".") + if len(parts) != 3 { + return "", fmt.Errorf("invalid jws signature") + } + + modifiedJWS := parts[0] + "." + parts[1] + "." + "8hiz2aWSW_AWnZ_GnoQyHrYgGia0HxdYTQGYOVYkPLU" + + return modifiedJWS, nil +} diff --git a/integration/internal/client/signer.go b/integration/internal/client/signer.go index 1095e63..1014800 100644 --- a/integration/internal/client/signer.go +++ b/integration/internal/client/signer.go @@ -170,6 +170,25 @@ func (s *Signer) CreatePresentation(data []byte) ([]byte, error) { return io.ReadAll(resp.Body) } +func (s *Signer) CreateCredential(data []byte) ([]byte, error) { + req, err := http.NewRequest(http.MethodPost, s.addr+"/v1/credential", bytes.NewReader(data)) + if err != nil { + return nil, err + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, newErrorResponse(resp) + } + + return io.ReadAll(resp.Body) +} + type errorResponse struct { Code int Status string diff --git a/internal/service/signer/service_test.go b/internal/service/signer/service_test.go index cdb6d1d..e7f724b 100644 --- a/internal/service/signer/service_test.go +++ b/internal/service/signer/service_test.go @@ -6,6 +6,7 @@ import ( "crypto/ecdsa" "encoding/base64" "encoding/json" + "fmt" "net/http" "testing" @@ -294,7 +295,7 @@ func TestService_CredentialProof(t *testing.T) { name: "credential with invalid subject id", credential: []byte(credentialWithInvalidSubjectID), errkind: errors.BadRequest, - errtext: "invalid format of subject id", + errtext: "invalid subject id: must be URI", }, { name: "valid credential but signer cannot find key", @@ -641,6 +642,168 @@ func TestService_PresentationProof(t *testing.T) { } } +func TestService_CreateCredential(t *testing.T) { + tests := []struct { + name string + signer *signerfakes.FakeVault + supportedKeys []string + + issuer string + namespace string + keyname string + credentialSubject map[string]interface{} + + errkind errors.Kind + errtext string + + contexts []string + types []string + proofPurpose string + proofType string + proofVerificationMethod string + wantedCredentialSubject verifiable.Subject + }{ + { + name: "missing credential subject", + errtext: "invalid credential subject: non-empty map is expected", + errkind: errors.BadRequest, + }, + { + name: "invalid credential subject id", + credentialSubject: map[string]interface{}{"id": "invalid credential subject id"}, + errtext: "invalid subject id: must be URI", + errkind: errors.BadRequest, + }, + { + name: "valid credential subject, but error finding signing key", + supportedKeys: []string{"ed25519", "ecdsa-p256"}, + issuer: "https://example.com", + namespace: "transit", + keyname: "key2", + credentialSubject: map[string]interface{}{"id": "https://example.com"}, + signer: &signerfakes.FakeVault{ + KeyStub: func(ctx context.Context, namespace, key string) (*signer.VaultKey, error) { + return nil, fmt.Errorf("no such key") + }, + }, + errtext: "error getting signing key: no such key", + errkind: errors.Unknown, + }, + { + name: "valid credential subject and signing is successful", + supportedKeys: []string{"ed25519", "ecdsa-p256"}, + issuer: "https://example.com", + namespace: "transit", + keyname: "key2", + credentialSubject: map[string]interface{}{"id": "https://example.com"}, + signer: &signerfakes.FakeVault{ + KeyStub: func(ctx context.Context, namespace, key string) (*signer.VaultKey, error) { + return &signer.VaultKey{ + Name: "key2", + Type: "ecdsa-p256", + }, nil + }, + WithKeyStub: func(namespace, key string) signer.Vault { + return &signerfakes.FakeVault{ + SignStub: func(data []byte) ([]byte, error) { + return []byte("test signature"), nil + }, + } + }, + }, + // expected attributes the VC must have + contexts: []string{ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/security/suites/jws-2020/v1", + "https://schema.org", + }, + types: []string{verifiable.VCType}, + proofPurpose: "assertionMethod", + proofType: "JsonWebSignature2020", + proofVerificationMethod: "https://example.com#key2", + wantedCredentialSubject: verifiable.Subject{ + ID: "https://example.com", + CustomFields: map[string]interface{}{}, + }, + }, + { + name: "valid credential with multiple claims and signing is successful", + supportedKeys: []string{"ed25519", "ecdsa-p256"}, + issuer: "https://example.com", + namespace: "transit", + keyname: "key2", + credentialSubject: map[string]interface{}{"id": "https://example.com", "email": "test@mymail.com"}, + signer: &signerfakes.FakeVault{ + KeyStub: func(ctx context.Context, namespace, key string) (*signer.VaultKey, error) { + return &signer.VaultKey{ + Name: "key2", + Type: "ecdsa-p256", + }, nil + }, + WithKeyStub: func(namespace, key string) signer.Vault { + return &signerfakes.FakeVault{ + SignStub: func(data []byte) ([]byte, error) { + return []byte("test signature"), nil + }, + } + }, + }, + // expected attributes the VC must have + contexts: []string{ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/security/suites/jws-2020/v1", + "https://schema.org", + }, + types: []string{verifiable.VCType}, + proofPurpose: "assertionMethod", + proofType: "JsonWebSignature2020", + proofVerificationMethod: "https://example.com#key2", + wantedCredentialSubject: verifiable.Subject{ + ID: "https://example.com", + CustomFields: map[string]interface{}{ + "email": "test@mymail.com", + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + signer := signer.New(test.signer, test.supportedKeys, http.DefaultClient, zap.NewNop()) + + req := &goasigner.CreateCredentialRequest{ + Issuer: test.issuer, + Namespace: test.namespace, + Key: test.keyname, + CredentialSubject: test.credentialSubject, + } + + credential, err := signer.CreateCredential(context.Background(), req) + if err != nil { + require.NotEmpty(t, test.errtext, "received error, but test case has no error: %v", err) + assert.Contains(t, err.Error(), test.errtext) + if e, ok := err.(*errors.Error); ok { + assert.Equal(t, test.errkind, e.Kind) + } + assert.Nil(t, credential) + } else { + require.Empty(t, test.errtext, "test case expects error, but got none") + assert.NotNil(t, credential) + + vc, ok := credential.(*verifiable.Credential) + assert.True(t, ok) + assert.Equal(t, test.contexts, vc.Context) + assert.Equal(t, test.types, vc.Types) + assert.Equal(t, test.proofPurpose, vc.Proofs[0]["proofPurpose"]) + assert.Equal(t, test.proofType, vc.Proofs[0]["type"]) + assert.Equal(t, test.proofVerificationMethod, vc.Proofs[0]["verificationMethod"]) + assert.NotEmpty(t, vc.Proofs[0]["jws"]) + assert.Equal(t, test.wantedCredentialSubject, vc.Subject) + } + }) + } +} + // ---------- Verifiable Credentials ---------- // //nolint:gosec -- GitLab