Repository: TBD54566975/ssi-service
Pull request #553 (2023-06-21 15:43:51)

Adding optional evidence field to credential creation
# Overview
Adding optional evidence field to create credential

# Description
Adding evidence as described here - https://www.w3.org/TR/vc-data-model/#evidence
fixes - https://github.com/TBD54566975/ssi-service/issues/550

```
{
  "@context": [
    "https://www.w3.org/2018/credentials/v1",
    "https://www.w3.org/2018/credentials/examples/v1"
  ],
  "id": "http://example.edu/credentials/3732",
  "type": ["VerifiableCredential", "UniversityDegreeCredential"],
  "issuer": "https://example.edu/issuers/14",
  "issuanceDate": "2010-01-01T19:23:24Z",
  "credentialSubject": {
    "id": "did:example:ebfeb1f712ebc6f1c276e12ec21",
    "degree": {
      "type": "BachelorDegree",
      "name": "Bachelor of Science and Arts"
    }
  },
  "evidence": [{
    "id": "https://example.edu/evidence/f2aeec97-fc0d-42bf-8ca7-0548192d4231",
    "type": ["DocumentVerification"],
    "verifier": "https://example.edu/issuers/14",
    "evidenceDocument": "DriversLicense",
    "subjectPresence": "Physical",
    "documentPresence": "Physical",
    "licenseNumber": "123AB4567"
  }],
  "proof": {  }
}
```

# How Has This Been Tested?
* Added to integration test
* Added to unit test



nitro-neal: Should we add validation code to the sdk?
https://github.com/TBD54566975/ssi-sdk/blob/main/credential/model.go#L29

It seems like the only validation that is required from the spec is:

```
The precise content of each evidence scheme is determined by the specific evidence [type](https://www.w3.org/TR/vc-data-model/#dfn-type) definition.
```

<img width="840" alt="image" src="https://github.com/TBD54566975/ssi-service/assets/5314059/613d5f96-0bc0-4857-a5e6-831df4391d03">

nitro-neal: @andresuribe87  Like only have "type": ["DocumentVerification"], and a few others? 
And then make sure the DocumentVerification only matches the matches the DocumentVerification schema?

Do you know where can I find what DocumentVerification is and what fields it requires?
andresuribe87: > Like only have "type": ["DocumentVerification"], and a few others?

Similar. To be precise, the only `type` I see us supporting is what's specified in https://docs.google.com/document/d/1y65HA9ZP58ppySG-yiBhGdNwsFrJZ8_-zupOyasuqgk/edit#heading=h.pvfljv28woox (i.e. `https://openid.bitbucket.io/ekyc/openid-connect-4-identity-assurance.html#name-verification-element`). 

> Do you know where can I find what DocumentVerification is and what fields it requires?

AFAIK, no one has implemented the `DocumentVerification` type. We should implement the type described above, since that's going to be used for KYC purposes. 

decentralgabe: I don't think we should implement a specific type until it's in the VC specs directory. I'm in favor of leaving it more generic for now.
andresuribe87: > I don't think we should implement a specific type until it's in the VC specs directory. I'm in favor of leaving it more generic for now.

SGTM
codecov-commenter: ## [Codecov](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/553?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) Report
> Merging [#553](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/553?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) (1c6ee3c) into [main](https://app.codecov.io/gh/TBD54566975/ssi-service/commit/3761db207d6859ee83513b3b48e22fd7486c3a04?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) (3761db2) will **decrease** coverage by `0.03%`.
> The diff coverage is `0.00%`.

```diff
@@            Coverage Diff             @@
##             main     #553      +/-   ##
==========================================
- Coverage   20.00%   19.97%   -0.03%     
==========================================
  Files          50       50              
  Lines        6175     6182       +7     
==========================================
  Hits         1235     1235              
- Misses       4712     4719       +7     
  Partials      228      228              
```


| [Impacted Files](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/553?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) | Coverage Δ | |
|---|---|---|
| [pkg/server/router/credential.go](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/553?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-cGtnL3NlcnZlci9yb3V0ZXIvY3JlZGVudGlhbC5nbw==) | `2.93% <0.00%> (-0.02%)` | :arrow_down: |
| [pkg/service/credential/model.go](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/553?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-cGtnL3NlcnZpY2UvY3JlZGVudGlhbC9tb2RlbC5nbw==) | `0.00% <0.00%> (ø)` | |
| [pkg/service/credential/service.go](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/553?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-cGtnL3NlcnZpY2UvY3JlZGVudGlhbC9zZXJ2aWNlLmdv) | `0.00% <0.00%> (ø)` | |



Patch:
diff --git a/doc/swagger.yaml b/doc/swagger.yaml
index a3e4046..e16c1c8 100644
--- a/doc/swagger.yaml
+++ b/doc/swagger.yaml
@@ -988,6 +988,14 @@ definitions:
         example:
           alumniOf: did_for_uni
         type: object
+      evidence:
+        additionalProperties:
+          type: string
+        description: Optional. Corresponds to `evidence` in https://www.w3.org/TR/vc-data-model-2.0/#evidence
+        example:
+          ' {''type''': ' [''DocumentVerification''}]'
+          '[{''id''': ' ''https://example.edu/evidence/f2aeec97-fc0d-42bf-8ca7-0548192d4231''}'
+        type: object
       expiry:
         description: Optional. Corresponds to `expirationDate` in https://www.w3.org/TR/vc-data-model/#expiration.
         example: "2029-01-01T19:23:24Z"
diff --git a/pkg/server/router/credential.go b/pkg/server/router/credential.go
index 98ef15c..4d7935d 100644
--- a/pkg/server/router/credential.go
+++ b/pkg/server/router/credential.go
@@ -7,7 +7,6 @@ import (
 	credsdk "github.com/TBD54566975/ssi-sdk/credential"
 	"github.com/gin-gonic/gin"
 	"github.com/pkg/errors"
-
 	credmodel "github.com/tbd54566975/ssi-service/internal/credential"
 	"github.com/tbd54566975/ssi-service/internal/keyaccess"
 	"github.com/tbd54566975/ssi-service/internal/util"
@@ -125,6 +124,9 @@ type CreateCredentialRequest struct {
 	// Whether this credential can be suspended. When true, the created VC will have the "credentialStatus"
 	// property set.
 	Suspendable bool `json:"suspendable,omitempty" example:"false"`
+
+	// Optional. Corresponds to `evidence` in https://www.w3.org/TR/vc-data-model-2.0/#evidence
+	Evidence []any `json:"evidence" swaggertype:"object,string" example:"[{'id': 'https://example.edu/evidence/f2aeec97-fc0d-42bf-8ca7-0548192d4231'}, {'type': ['DocumentVerification'}]"`
 	// TODO(gabe) support more capabilities like signature type, format, and more.
 }
 
@@ -139,6 +141,7 @@ func (c CreateCredentialRequest) toServiceRequest() credential.CreateCredentialR
 		Expiry:      c.Expiry,
 		Revocable:   c.Revocable,
 		Suspendable: c.Suspendable,
+		Evidence:    c.Evidence,
 	}
 }
 
diff --git a/pkg/service/credential/model.go b/pkg/service/credential/model.go
index 1304471..f71c459 100644
--- a/pkg/service/credential/model.go
+++ b/pkg/service/credential/model.go
@@ -1,6 +1,8 @@
 package credential
 
 import (
+	"fmt"
+
 	"github.com/tbd54566975/ssi-service/internal/credential"
 )
 
@@ -24,6 +26,7 @@ type CreateCredentialRequest struct {
 	Expiry      string         `json:"expiry,omitempty"`
 	Revocable   bool           `json:"revocable,omitempty"`
 	Suspendable bool           `json:"suspendable,omitempty"`
+	Evidence    []any          `json:"evidence,omitempty"`
 	// TODO(gabe) support more capabilities like signature type, format, evidence, and more.
 }
 
@@ -99,3 +102,25 @@ func (csr CreateCredentialRequest) isStatusValid() bool {
 func (csr CreateCredentialRequest) hasStatus() bool {
 	return csr.Suspendable || csr.Revocable
 }
+
+func (csr CreateCredentialRequest) hasEvidence() bool {
+	return len(csr.Evidence) != 0
+}
+
+func (csr *CreateCredentialRequest) validateEvidence() error {
+	for _, e := range csr.Evidence {
+		evidenceMap, ok := e.(map[string]any)
+		if !ok {
+			return fmt.Errorf("invalid evidence format")
+		}
+
+		_, idExists := evidenceMap["id"]
+		_, typeExists := evidenceMap["type"]
+
+		if !idExists || !typeExists {
+			return fmt.Errorf("evidence missing required 'id' or 'type' field")
+		}
+	}
+
+	return nil
+}
diff --git a/pkg/service/credential/service.go b/pkg/service/credential/service.go
index 3cbb012..4277400 100644
--- a/pkg/service/credential/service.go
+++ b/pkg/service/credential/service.go
@@ -204,6 +204,16 @@ func (s Service) createCredential(ctx context.Context, request CreateCredentialR
 		}
 	}
 
+	if request.hasEvidence() {
+		if err := request.validateEvidence(); err != nil {
+			return nil, sdkutil.LoggingErrorMsg(err, "could validate evidence")
+		}
+
+		if err := builder.SetEvidence(request.Evidence); err != nil {
+			return nil, sdkutil.LoggingErrorMsg(err, "could not set evidence")
+		}
+	}
+
 	cred, err := builder.Build()
 	if err != nil {
 		return nil, sdkutil.LoggingErrorMsg(err, "could not build credential")


Test patch:
diff --git a/integration/testdata/credential-input.json b/integration/testdata/credential-input.json
index f528e15..232bfc0 100644
--- a/integration/testdata/credential-input.json
+++ b/integration/testdata/credential-input.json
@@ -20,5 +20,14 @@
   "expiry": "2051-10-05T14:48:00.000Z",
   "schemaId": "{{.SchemaID}}",
   "revocable": {{.Revocable}},
-  "suspendable": {{.Suspendable}}
+  "suspendable": {{.Suspendable}},
+  "evidence": [{
+    "id": "https://example.edu/evidence/f2aeec97-fc0d-42bf-8ca7-0548192d4231",
+    "type": ["DocumentVerification"],
+    "verifier": "https://example.edu/issuers/14",
+    "evidenceDocument": "DriversLicense",
+    "subjectPresence": "Physical",
+    "documentPresence": "Physical",
+    "licenseNumber": "123AB4567"
+  }]
 }
\ No newline at end of file
diff --git a/pkg/server/router/credential_test.go b/pkg/server/router/credential_test.go
index 75e4d77..e7ab601 100644
--- a/pkg/server/router/credential_test.go
+++ b/pkg/server/router/credential_test.go
@@ -79,6 +79,7 @@ func TestCredentialRouter(t *testing.T) {
 				assert.NoError(tt, err)
 				assert.NotEmpty(tt, createdCred)
 				assert.NotEmpty(tt, createdCred.CredentialJWT)
+				assert.Empty(tt, createdCred.Credential.Evidence)
 
 				cred := createdCred.Credential
 
@@ -1023,6 +1024,76 @@ func TestCredentialRouter(t *testing.T) {
 				assert.Error(tt, err)
 				assert.ErrorContains(tt, err, "has a different status purpose<revocation> value than the status credential<suspension>")
 			})
+
+			t.Run("Create Credential With Invalid Evidence", func(tt *testing.T) {
+				issuer, issuerKID, schemaID, credService := createCredServicePrereqs(tt, test.ServiceStorage(t))
+				subject := "did:test:345"
+
+				_, err := credService.CreateCredential(context.Background(), credential.CreateCredentialRequest{
+					Issuer:    issuer,
+					IssuerKID: issuerKID,
+					Subject:   subject,
+					SchemaID:  schemaID,
+					Data: map[string]any{
+						"email": "Satoshi@Nakamoto.btc",
+					},
+					Expiry:   time.Now().Add(24 * time.Hour).Format(time.RFC3339),
+					Evidence: []any{"hi", 123, true},
+				})
+
+				assert.ErrorContains(tt, err, "invalid evidence format")
+			})
+
+			t.Run("Create Credential With Invalid Evidence No Id", func(tt *testing.T) {
+				issuer, issuerKID, schemaID, credService := createCredServicePrereqs(tt, test.ServiceStorage(t))
+				subject := "did:test:345"
+
+				evidenceMap := map[string]any{
+					"type":             []string{"DocumentVerification"},
+					"verifier":         "https://example.edu/issuers/14",
+					"evidenceDocument": "DriversLicense",
+					"subjectPresence":  "Physical",
+					"documentPresence": "Physical",
+					"licenseNumber":    "123AB4567",
+				}
+
+				_, err := credService.CreateCredential(context.Background(), credential.CreateCredentialRequest{
+					Issuer:    issuer,
+					IssuerKID: issuerKID,
+					Subject:   subject,
+					SchemaID:  schemaID,
+					Data: map[string]any{
+						"email": "Satoshi@Nakamoto.btc",
+					},
+					Expiry:   time.Now().Add(24 * time.Hour).Format(time.RFC3339),
+					Evidence: []any{evidenceMap},
+				})
+
+				assert.ErrorContains(tt, err, "missing required 'id' or 'type'")
+			})
+
+			t.Run("Create Credential With Evidence", func(tt *testing.T) {
+				issuer, issuerKID, schemaID, credService := createCredServicePrereqs(tt, test.ServiceStorage(t))
+				subject := "did:test:345"
+
+				createdCred, err := credService.CreateCredential(context.Background(), credential.CreateCredentialRequest{
+					Issuer:    issuer,
+					IssuerKID: issuerKID,
+					Subject:   subject,
+					SchemaID:  schemaID,
+					Data: map[string]any{
+						"email": "Satoshi@Nakamoto.btc",
+					},
+					Expiry:   time.Now().Add(24 * time.Hour).Format(time.RFC3339),
+					Evidence: getEvidence(),
+				})
+
+				assert.NoError(tt, err)
+				assert.NotEmpty(tt, createdCred)
+				assert.NotEmpty(tt, createdCred.CredentialJWT)
+
+				assert.ElementsMatch(tt, createdCred.Credential.Evidence, getEvidence())
+			})
 		})
 	}
 }
@@ -1076,3 +1147,16 @@ func getEmailSchema() map[string]any {
 		},
 	}
 }
+
+func getEvidence() []any {
+	evidenceMap := map[string]any{
+		"id":               "https://example.edu/evidence/f2aeec97-fc0d-42bf-8ca7-0548192d4231",
+		"type":             []string{"DocumentVerification"},
+		"verifier":         "https://example.edu/issuers/14",
+		"evidenceDocument": "DriversLicense",
+		"subjectPresence":  "Physical",
+		"documentPresence": "Physical",
+		"licenseNumber":    "123AB4567",
+	}
+	return []any{evidenceMap}
+}