From 671f9390b04e1c25bad44ed14b455bd8cbf65b23 Mon Sep 17 00:00:00 2001
From: Lyuben Penkovski <lyuben.penkovski@vereign.com>
Date: Fri, 27 Oct 2023 12:26:35 +0300
Subject: [PATCH] Support create/verify for multiple proofs on VC

---
 integration/credentials_testdata.go   |   8 +-
 integration/integration_test.go       | 157 ++++++++++++++++++++++++--
 integration/internal/client/signer.go |  10 +-
 integration/presentations_testdata.go |  30 ++---
 internal/service/signer/service.go    |   8 +-
 5 files changed, 181 insertions(+), 32 deletions(-)

diff --git a/integration/credentials_testdata.go b/integration/credentials_testdata.go
index 79d9042..eec0604 100644
--- a/integration/credentials_testdata.go
+++ b/integration/credentials_testdata.go
@@ -9,7 +9,7 @@ var credentialWithSubjectID = `
         "https://schema.org"
     ],
     "type": "VerifiableCredential",
-    "issuer": "did:web:4db4-85-196-181-2.eu.ngrok.io:policy:policy:example:returnDID:1.0:evaluation",
+    "issuer": "did:web:comic-bullfrog-smoothly.ngrok-free.app:policy:example:returnDID:1.0:evaluation",
     "issuanceDate": "2010-01-01T19:23:24Z",
     "credentialSubject":
     {
@@ -27,7 +27,7 @@ var credentialWithoutSubjectID = `
         "https://schema.org"
     ],
     "type": "VerifiableCredential",
-    "issuer": "did:web:4db4-85-196-181-2.eu.ngrok.io:policy:policy:example:returnDID:1.0:evaluation",
+    "issuer": "did:web:comic-bullfrog-smoothly.ngrok-free.app:policy:example:returnDID:1.0:evaluation",
     "issuanceDate": "2010-01-01T19:23:24Z",
     "credentialSubject":
     {
@@ -44,7 +44,7 @@ var credentialInvalidSubjectID = `
         "https://schema.org"
     ],
     "type": "VerifiableCredential",
-    "issuer": "did:web:4db4-85-196-181-2.eu.ngrok.io:policy:policy:example:returnDID:1.0:evaluation",
+    "issuer": "did:web:comic-bullfrog-smoothly.ngrok-free.app:policy:example:returnDID:1.0:evaluation",
     "issuanceDate": "2010-01-01T19:23:24Z",
     "credentialSubject":
     {
@@ -62,7 +62,7 @@ var credentialWithNumericalSubjectID = `
         "https://schema.org"
     ],
     "type": "VerifiableCredential",
-    "issuer": "did:web:4db4-85-196-181-2.eu.ngrok.io:policy:policy:example:returnDID:1.0:evaluation",
+    "issuer": "did:web:comic-bullfrog-smoothly.ngrok-free.app:policy:example:returnDID:1.0:evaluation",
     "issuanceDate": "2010-01-01T19:23:24Z",
     "credentialSubject":
     {
diff --git a/integration/integration_test.go b/integration/integration_test.go
index 250d853..568adb9 100644
--- a/integration/integration_test.go
+++ b/integration/integration_test.go
@@ -3,6 +3,9 @@ package integration
 import (
 	"encoding/json"
 	"fmt"
+	"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"
 	"net/http"
 	"os"
 	"testing"
@@ -54,7 +57,7 @@ func TestCreateAndVerifyCredentialProof(t *testing.T) {
 			signer := client.NewSigner(addr)
 
 			// create proof
-			vcWithProof, err := signer.CreateCredentialProof(test.vc)
+			vcWithProof, err := signer.CreateCredentialProof("key1", test.vc)
 			assert.NoError(t, err)
 			assert.NotNil(t, vcWithProof)
 
@@ -132,7 +135,7 @@ func TestCreateCredentialProof(t *testing.T) {
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
 			signer := client.NewSigner(addr)
-			vcWithProof, err := signer.CreateCredentialProof(test.vc)
+			vcWithProof, err := signer.CreateCredentialProof("key1", test.vc)
 			if test.errMsg != "" {
 				require.Error(t, err, fmt.Sprintf("got no error but expected %q", test.errMsg))
 				assert.Contains(t, err.Error(), test.errMsg)
@@ -183,7 +186,7 @@ func TestCreateAndVerifyPresentationProof(t *testing.T) {
 			signer := client.NewSigner(addr)
 
 			// create proof
-			vpWithProof, err := signer.CreatePresentationProof(test.vp)
+			vpWithProof, err := signer.CreatePresentationProof("key1", test.vp)
 			require.NoError(t, err)
 			assert.NotNil(t, vpWithProof)
 
@@ -269,7 +272,7 @@ func TestCreatePresentationProof(t *testing.T) {
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
 			signer := client.NewSigner(addr)
-			vpWithProof, err := signer.CreatePresentationProof(test.vp)
+			vpWithProof, err := signer.CreatePresentationProof("key1", test.vp)
 			if test.errMsg != "" {
 				require.Error(t, err, fmt.Sprintf("got no error but expected %q", test.errMsg))
 				assert.Contains(t, err.Error(), test.errMsg)
@@ -320,7 +323,7 @@ func TestCreatePresentation(t *testing.T) {
 		{
 			name: "valid request with single credentialSubject entry",
 			req: map[string]interface{}{
-				"issuer":    "did:web:4db4-85-196-181-2.eu.ngrok.io:policy:policy:example:returnDID:1.0:evaluation",
+				"issuer":    "did:web:comic-bullfrog-smoothly.ngrok-free.app:policy:example:returnDID:1.0:evaluation",
 				"namespace": "transit",
 				"key":       "key1",
 				"data": []map[string]interface{}{
@@ -331,7 +334,7 @@ func TestCreatePresentation(t *testing.T) {
 		{
 			name: "valid request with multiple credentialSubject entry",
 			req: map[string]interface{}{
-				"issuer":    "did:web:4db4-85-196-181-2.eu.ngrok.io:policy:policy:example:returnDID:1.0:evaluation",
+				"issuer":    "did:web:comic-bullfrog-smoothly.ngrok-free.app:policy:example:returnDID:1.0:evaluation",
 				"namespace": "transit",
 				"key":       "key1",
 				"data": []map[string]interface{}{
@@ -343,7 +346,7 @@ func TestCreatePresentation(t *testing.T) {
 		{
 			name: "valid request with additional context",
 			req: map[string]interface{}{
-				"issuer":    "did:web:4db4-85-196-181-2.eu.ngrok.io:policy:policy:example:returnDID:1.0:evaluation",
+				"issuer":    "did:web:comic-bullfrog-smoothly.ngrok-free.app:policy:example:returnDID:1.0:evaluation",
 				"namespace": "transit",
 				"key":       "key1",
 				"context": []string{
@@ -402,3 +405,143 @@ func TestCreatePresentation(t *testing.T) {
 		})
 	}
 }
+
+func TestCreateCredentialMultipleProofs(t *testing.T) {
+	initTests(t)
+
+	// first create a credential with one proof
+	signer := client.NewSigner(addr)
+
+	cred := []byte(credentialWithSubjectID)
+
+	// create first proof
+	vcWithProof, err := signer.CreateCredentialProof("key1", cred)
+	assert.NoError(t, err)
+	assert.NotNil(t, vcWithProof)
+
+	// verify signature
+	_, err = verifyCredentialProofs(vcWithProof)
+	assert.NoError(t, err)
+
+	// create second proof
+	vc2Proofs, err := signer.CreateCredentialProof("key1", vcWithProof)
+	assert.NoError(t, err)
+	assert.NotNil(t, vc2Proofs)
+
+	// verify signatures
+	_, err = verifyCredentialProofs(vc2Proofs)
+	require.NoError(t, err)
+
+	// run tests modifying the contents of the VC and do proof verifications afterwards
+	t.Run("modify credential subject and check proofs afterwards", func(t *testing.T) {
+		correctVC := make([]byte, len(vc2Proofs))
+		copy(correctVC, vc2Proofs)
+
+		parsedVC, err := verifyCredentialProofs(correctVC)
+		assert.NoError(t, err)
+
+		// modify the credentialSubject by adding a new value
+		// which MUST break signature verification
+		subject, ok := parsedVC.Subject.([]verifiable.Subject)
+		assert.True(t, ok)
+
+		subject[0].CustomFields["newKey"] = "newValue"
+
+		// marshal the modified credential
+		modifiedVC, err := json.Marshal(parsedVC)
+		assert.NoError(t, err)
+		assert.NotNil(t, modifiedVC)
+
+		_, err = verifyCredentialProofs(modifiedVC)
+		assert.Error(t, err)
+		assert.Contains(t, err.Error(), "invalid signature")
+	})
+
+	t.Run("modify first signature and check proofs afterwards", func(t *testing.T) {
+		correctVC := make([]byte, len(vc2Proofs))
+		copy(correctVC, vc2Proofs)
+
+		parsedVC, err := verifyCredentialProofs(correctVC)
+		assert.NoError(t, err)
+
+		// 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
+		} else {
+			t.Errorf("expected to have proof 1 but it's missing or invalid")
+		}
+
+		// marshal the modified credential
+		modifiedVC, err := json.Marshal(parsedVC)
+		assert.NoError(t, err)
+		assert.NotNil(t, modifiedVC)
+
+		_, err = verifyCredentialProofs(modifiedVC)
+		assert.Error(t, err)
+		assert.Contains(t, err.Error(), "invalid signature")
+	})
+
+	t.Run("modifiy second signature and check proofs afterwards", func(t *testing.T) {
+		correctVC := make([]byte, len(vc2Proofs))
+		copy(correctVC, vc2Proofs)
+
+		parsedVC, err := verifyCredentialProofs(correctVC)
+		assert.NoError(t, err)
+
+		// 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
+		} else {
+			t.Errorf("expected to have proof 2 but it's missing or invalid")
+		}
+
+		// marshal the modified credential
+		modifiedVC, err := json.Marshal(parsedVC)
+		assert.NoError(t, err)
+		assert.NotNil(t, modifiedVC)
+
+		_, err = verifyCredentialProofs(modifiedVC)
+		assert.Error(t, err)
+		assert.Contains(t, err.Error(), "invalid signature")
+	})
+}
+
+func verifyCredentialProofs(vcBytes []byte) (*verifiable.Credential, error) {
+	webVDR := web.New()
+	keyVDR := key.New()
+	registry := vdr.New(
+		vdr.WithVDR(webVDR),
+		vdr.WithVDR(keyVDR),
+	)
+	keyResolver := verifiable.NewVDRKeyResolver(registry)
+
+	// parse it to object to modify credentialSubject attribute
+	vc, err := verifiable.ParseCredential(
+		vcBytes,
+		verifiable.WithJSONLDDocumentLoader(loader),
+		verifiable.WithStrictValidation(),
+		verifiable.WithJSONLDValidation(),
+		verifiable.WithJSONLDOnlyValidRDF(),
+		verifiable.WithPublicKeyFetcher(keyResolver.PublicKeyFetcher()),
+	)
+
+	return vc, err
+}
diff --git a/integration/internal/client/signer.go b/integration/internal/client/signer.go
index 9e7fa36..1095e63 100644
--- a/integration/internal/client/signer.go
+++ b/integration/internal/client/signer.go
@@ -16,7 +16,7 @@ func NewSigner(addr string) *Signer {
 	return &Signer{addr: addr}
 }
 
-func (s *Signer) CreateCredentialProof(vc []byte) ([]byte, error) {
+func (s *Signer) CreateCredentialProof(key string, vc []byte) ([]byte, error) {
 	var cred map[string]interface{}
 	if err := json.Unmarshal(vc, &cred); err != nil {
 		return nil, err
@@ -24,7 +24,7 @@ func (s *Signer) CreateCredentialProof(vc []byte) ([]byte, error) {
 
 	payload := map[string]interface{}{
 		"namespace":  "transit",
-		"key":        "key2",
+		"key":        key,
 		"credential": cred,
 	}
 
@@ -83,16 +83,16 @@ func (s *Signer) VerifyCredentialProof(vc []byte) error {
 	return nil
 }
 
-func (s *Signer) CreatePresentationProof(vp []byte) ([]byte, error) {
+func (s *Signer) CreatePresentationProof(key string, vp []byte) ([]byte, error) {
 	var pres map[string]interface{}
 	if err := json.Unmarshal(vp, &pres); err != nil {
 		return nil, err
 	}
 
 	payload := map[string]interface{}{
-		"issuer":       "did:web:4db4-85-196-181-2.eu.ngrok.io:policy:policy:example:returnDID:1.0:evaluation",
+		"issuer":       "did:web:comic-bullfrog-smoothly.ngrok-free.app:policy:example:returnDID:1.0:evaluation",
 		"namespace":    "transit",
-		"key":          "key2",
+		"key":          key,
 		"presentation": pres,
 	}
 
diff --git a/integration/presentations_testdata.go b/integration/presentations_testdata.go
index bc3dbef..8ba596b 100644
--- a/integration/presentations_testdata.go
+++ b/integration/presentations_testdata.go
@@ -8,7 +8,7 @@ var presentationWithSubjectID = `
 		"https://w3id.org/security/suites/jws-2020/v1",
 		"https://schema.org"
     ],
-    "id": "did:web:4db4-85-196-181-2.eu.ngrok.io:policy:policy:example:returnDID:1.0:evaluation",
+    "id": "did:web:comic-bullfrog-smoothly.ngrok-free.app:policy:example:returnDID:1.0:evaluation",
     "type": "VerifiablePresentation",
     "verifiableCredential":
     [
@@ -18,9 +18,9 @@ var presentationWithSubjectID = `
 				"https://www.w3.org/2018/credentials/v1",
 				"https://schema.org"
             ],
-            "id": "did:web:4db4-85-196-181-2.eu.ngrok.io:policy:policy:example:returnDID:1.0:evaluation",
+            "id": "did:web:comic-bullfrog-smoothly.ngrok-free.app:policy:example:returnDID:1.0:evaluation",
             "type": "VerifiableCredential",
-            "issuer": "did:web:4db4-85-196-181-2.eu.ngrok.io:policy:policy:example:returnDID:1.0:evaluation",
+            "issuer": "did:web:comic-bullfrog-smoothly.ngrok-free.app:policy:example:returnDID:1.0:evaluation",
             "issuanceDate": "2010-01-01T19:23:24Z",
             "credentialSubject":
             {
@@ -39,7 +39,7 @@ var presentationWithoutSubjectID = `
 		"https://w3id.org/security/suites/jws-2020/v1",
 		"https://schema.org"
     ],
-    "id": "did:web:4db4-85-196-181-2.eu.ngrok.io:policy:policy:example:returnDID:1.0:evaluation",
+    "id": "did:web:comic-bullfrog-smoothly.ngrok-free.app:policy:example:returnDID:1.0:evaluation",
     "type": "VerifiablePresentation",
     "verifiableCredential":
     [
@@ -49,9 +49,9 @@ var presentationWithoutSubjectID = `
 				"https://www.w3.org/2018/credentials/v1",
 				"https://schema.org"
             ],
-            "id": "did:web:4db4-85-196-181-2.eu.ngrok.io:policy:policy:example:returnDID:1.0:evaluation",
+            "id": "did:web:comic-bullfrog-smoothly.ngrok-free.app:policy:example:returnDID:1.0:evaluation",
             "type": "VerifiableCredential",
-            "issuer": "did:web:4db4-85-196-181-2.eu.ngrok.io:policy:policy:example:returnDID:1.0:evaluation",
+            "issuer": "did:web:comic-bullfrog-smoothly.ngrok-free.app:policy:example:returnDID:1.0:evaluation",
             "issuanceDate": "2010-01-01T19:23:24Z",
             "credentialSubject":
             {
@@ -69,7 +69,7 @@ var presentationWithInvalidSubjectID = `
 		"https://w3id.org/security/suites/jws-2020/v1",
 		"https://schema.org"
     ],
-    "id": "did:web:4db4-85-196-181-2.eu.ngrok.io:policy:policy:example:returnDID:1.0:evaluation",
+    "id": "did:web:comic-bullfrog-smoothly.ngrok-free.app:policy:example:returnDID:1.0:evaluation",
     "type": "VerifiablePresentation",
     "verifiableCredential":
     [
@@ -79,9 +79,9 @@ var presentationWithInvalidSubjectID = `
 				"https://www.w3.org/2018/credentials/v1",
 				"https://schema.org"
             ],
-            "id": "did:web:4db4-85-196-181-2.eu.ngrok.io:policy:policy:example:returnDID:1.0:evaluation",
+            "id": "did:web:comic-bullfrog-smoothly.ngrok-free.app:policy:example:returnDID:1.0:evaluation",
             "type": "VerifiableCredential",
-            "issuer": "did:web:4db4-85-196-181-2.eu.ngrok.io:policy:policy:example:returnDID:1.0:evaluation",
+            "issuer": "did:web:comic-bullfrog-smoothly.ngrok-free.app:policy:example:returnDID:1.0:evaluation",
             "issuanceDate": "2010-01-01T19:23:24Z",
             "credentialSubject":
             {
@@ -100,7 +100,7 @@ var presentationWithNumericalSubjectID = `
 		"https://w3id.org/security/suites/jws-2020/v1",
 		"https://schema.org"
     ],
-    "id": "did:web:4db4-85-196-181-2.eu.ngrok.io:policy:policy:example:returnDID:1.0:evaluation",
+    "id": "did:web:comic-bullfrog-smoothly.ngrok-free.app:policy:example:returnDID:1.0:evaluation",
     "type": "VerifiablePresentation",
     "verifiableCredential":
     [
@@ -110,9 +110,9 @@ var presentationWithNumericalSubjectID = `
 				"https://www.w3.org/2018/credentials/v1",
 				"https://schema.org"
             ],
-            "id": "did:web:4db4-85-196-181-2.eu.ngrok.io:policy:policy:example:returnDID:1.0:evaluation",
+            "id": "did:web:comic-bullfrog-smoothly.ngrok-free.app:policy:example:returnDID:1.0:evaluation",
             "type": "VerifiableCredential",
-            "issuer": "did:web:4db4-85-196-181-2.eu.ngrok.io:policy:policy:example:returnDID:1.0:evaluation",
+            "issuer": "did:web:comic-bullfrog-smoothly.ngrok-free.app:policy:example:returnDID:1.0:evaluation",
             "issuanceDate": "2010-01-01T19:23:24Z",
             "credentialSubject":
             {
@@ -131,7 +131,7 @@ var presentationWithMissingCredentialContext = `
 		"https://w3id.org/security/suites/jws-2020/v1",
 		"https://schema.org"
     ],
-    "id": "did:web:gaiax.vereign.com:tsa:policy:policy:example:returnDID:1.0:evaluation",
+    "id": "did:web:comic-bullfrog-smoothly.ngrok-free.app:policy:example:returnDID:1.0:evaluation",
     "type": "VerifiablePresentation",
     "verifiableCredential":
     [
@@ -146,10 +146,10 @@ var presentationWithMissingCredentialContext = `
                 "age_over": 18,
                 "allow": false,
                 "citizenship": "France",
-                "id": "https://gaiax.vereign.com/tsa/policy/example/ProofRequestResponse/1.0"
+                "id": "https://ssi-dev.vereign.com/tsa/policy/policy/example/example/1.0/evaluation"
             },
             "issuanceDate": "2022-07-21T10:24:36.203848291Z",
-            "issuer": "did:web:gaiax.vereign.com:tsa:policy:policy:example:returnDID:1.0:evaluation",
+            "issuer": "did:web:comic-bullfrog-smoothly.ngrok-free.app:policy:example:returnDID:1.0:evaluation",
             "type": "VerifiableCredential"
         }
     ]
diff --git a/internal/service/signer/service.go b/internal/service/signer/service.go
index fccbba5..e8fa2d6 100644
--- a/internal/service/signer/service.go
+++ b/internal/service/signer/service.go
@@ -211,6 +211,7 @@ func (s *Service) CredentialProof(ctx context.Context, req *signer.CredentialPro
 		return nil, errors.New(errors.BadRequest, err.Error())
 	}
 
+	// credential may not have a proof, so disable proofCheck on first round
 	vc, err := s.parseCredential(vcBytes, false)
 	if err != nil {
 		logger.Error("error parsing verifiable credential", zap.Error(err))
@@ -220,8 +221,13 @@ func (s *Service) CredentialProof(ctx context.Context, req *signer.CredentialPro
 		return nil, errors.New(errors.BadRequest, err.Error())
 	}
 
+	// if the given credential has at least one proof, check again to verify the proofs
 	if len(vc.Proofs) > 0 {
-		return nil, errors.New(errors.Forbidden, "credential already has proof")
+		vc, err = s.parseCredential(vcBytes, true)
+		if err != nil {
+			logger.Error("credential proofs cannot be verified", zap.Error(err))
+			return nil, errors.New(errors.Forbidden, err.Error())
+		}
 	}
 
 	if err := validateCredentialSubject(vc.Subject); err != nil {
-- 
GitLab