diff --git a/integration/credentials_testdata.go b/integration/credentials_testdata.go index 79d9042d76bd1eea9f015abaeff2df7a9c18a328..d24d84c54368479c7738a3fab713012d9f1f62e8 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:yourdomain.com:policy:policies: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:yourdomain.com:policy:policies: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:yourdomain.com:policy:policies: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:yourdomain.com:policy:policies: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 250d8539c31a7b6c139e819fe4d1df338ccf2171..c6504694e3a34a177c4947e27d369f353ad8ba1e 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -5,9 +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/piprate/json-gold/ld" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -54,7 +58,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) @@ -78,7 +82,7 @@ func TestCreateAndVerifyCredentialProof(t *testing.T) { assert.True(t, ok) // modify the credentialSubject by adding a new value - subject[0].CustomFields["newKey"] = "newValue" + subject[0].CustomFields["newKey"] = "newValue" // nolint:goconst // marshal the modified credential modifiedVC, err := json.Marshal(vc) @@ -115,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", @@ -132,7 +136,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 +187,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) @@ -252,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", @@ -269,7 +273,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) @@ -295,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:yourdomain.com:policy:policies: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:yourdomain.com:policy:policies: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) @@ -313,14 +401,16 @@ 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", }, { 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:yourdomain.com:policy:policies:example:returnDID:1.0:evaluation", "namespace": "transit", "key": "key1", "data": []map[string]interface{}{ @@ -331,7 +421,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:yourdomain.com:policy:policies:example:returnDID:1.0:evaluation", "namespace": "transit", "key": "key1", "data": []map[string]interface{}{ @@ -343,7 +433,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:yourdomain.com:policy:policies:example:returnDID:1.0:evaluation", "namespace": "transit", "key": "key1", "context": []string{ @@ -389,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{}) @@ -402,3 +492,142 @@ 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 != "" { + 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") + } + + // marshal the modified credential + modifiedVC, err := json.Marshal(parsedVC) + assert.NoError(t, err) + assert.NotNil(t, modifiedVC) + + _, err = verifyCredentialProofs(modifiedVC) + require.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 != "" { + 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") + } + + // 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 +} + +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 9e7fa3677923db5f3a8b5959841794bba6f38708..712aeddae429c8f64cd1e2697d23843fd4e6267f 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:yourdomain.com:policy:policies:example:returnDID:1.0:evaluation", "namespace": "transit", - "key": "key2", + "key": key, "presentation": pres, } @@ -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/integration/presentations_testdata.go b/integration/presentations_testdata.go index bc3dbef0971f249aae04069cc8337e7831f3fc64..9d513b865c4f104c70dde015945240a7cee95bf4 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:yourdomain.com:policy:policies: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:yourdomain.com:policy:policies: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:yourdomain.com:policy:policies: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:yourdomain.com:policy:policies: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:yourdomain.com:policy:policies: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:yourdomain.com:policy:policies: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:yourdomain.com:policy:policies: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:yourdomain.com:policy:policies: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:yourdomain.com:policy:policies: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:yourdomain.com:policy:policies: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:yourdomain.com:policy:policies: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:yourdomain.com:policy:policies: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:yourdomain.com:policy:policies: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:yourdomain.com:policy:policies:example:returnDID:1.0:evaluation", "type": "VerifiableCredential" } ]