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