Repository: TBD54566975/ssi-service
Pull request #148 (2022-10-27 03:00:25)

Uptake err handling for credential denial
- handle credential denial errrs
- added tests

codecov-commenter: # [Codecov](https://codecov.io/gh/TBD54566975/ssi-service/pull/148?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) Report
> Merging [#148](https://codecov.io/gh/TBD54566975/ssi-service/pull/148?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) (5228380) into [main](https://codecov.io/gh/TBD54566975/ssi-service/commit/3cf0bcbd483a43f16eb1201875c445fdde84168d?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) (3cf0bcb) will **not change** coverage.
> The diff coverage is `n/a`.

```diff
@@           Coverage Diff           @@
##             main     #148   +/-   ##
=======================================
  Coverage   27.79%   27.79%           
=======================================
  Files          20       20           
  Lines        1558     1558           
=======================================
  Hits          433      433           
  Misses       1051     1051           
  Partials       74       74           
```



Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975)


Patch:
diff --git a/go.sum b/go.sum
index feb8e3a..f7a6f82 100644
--- a/go.sum
+++ b/go.sum
@@ -12,8 +12,10 @@ github.com/TBD54566975/ssi-sdk v0.0.1-alpha.0.20221019161256-4b326c0c823a h1:YAd
 github.com/TBD54566975/ssi-sdk v0.0.1-alpha.0.20221019161256-4b326c0c823a/go.mod h1:WBOPlvZkJ1qXW5HINQiNxgWTEm5THpkPy8iHXNkYnRY=
 github.com/TBD54566975/ssi-sdk v0.0.1-alpha.0.20221019200812-2e73f0ac7722 h1:aDCoimfCRlAcFCYo3XUiYmavv39VXf45xX2eLrTgmtA=
 github.com/TBD54566975/ssi-sdk v0.0.1-alpha.0.20221019200812-2e73f0ac7722/go.mod h1:WBOPlvZkJ1qXW5HINQiNxgWTEm5THpkPy8iHXNkYnRY=
-github.com/TBD54566975/ssi-sdk v0.0.1-alpha.0.20221026213127-eea7099126ba h1:OExThhbLLwygWXI4txpVylufSI8UODokVcYJYdTI5yQ=
-github.com/TBD54566975/ssi-sdk v0.0.1-alpha.0.20221026213127-eea7099126ba/go.mod h1:Uvgv4WK66gwiV76SpWe6EUnUSwyH7r9EFqP0tTSDP18=
+github.com/TBD54566975/ssi-sdk v0.0.1-alpha.0.20221025160301-7220d5f4824b h1:R5qhf8ErN9A55FPIC0S8/iWko/5vCRJelG0WYikhmmo=
+github.com/TBD54566975/ssi-sdk v0.0.1-alpha.0.20221025160301-7220d5f4824b/go.mod h1:Uvgv4WK66gwiV76SpWe6EUnUSwyH7r9EFqP0tTSDP18=
+github.com/TBD54566975/ssi-sdk v0.0.2-alpha h1:6fO4EaW84OsJkt6Lc8ka2Vy1maAXXbcyb/jWkHeahwE=
+github.com/TBD54566975/ssi-sdk v0.0.2-alpha/go.mod h1:Uvgv4WK66gwiV76SpWe6EUnUSwyH7r9EFqP0tTSDP18=
 github.com/TBD54566975/ssi-sdk v0.0.2-alpha.0.20221027011419-39fefd7f233c h1:tMbaOaxyjlhV4X0ojUHVUS/Fa+/OQf8i1/V23uqdhmo=
 github.com/TBD54566975/ssi-sdk v0.0.2-alpha.0.20221027011419-39fefd7f233c/go.mod h1:Uvgv4WK66gwiV76SpWe6EUnUSwyH7r9EFqP0tTSDP18=
 github.com/ardanlabs/conf v1.5.0 h1:5TwP6Wu9Xi07eLFEpiCUF3oQXh9UzHMDVnD3u/I5d5c=
diff --git a/pkg/service/manifest/application.go b/pkg/service/manifest/application.go
index 90200ea..8e9606d 100644
--- a/pkg/service/manifest/application.go
+++ b/pkg/service/manifest/application.go
@@ -2,8 +2,10 @@ package manifest
 
 import (
 	"fmt"
+	"strings"
 
 	"github.com/TBD54566975/ssi-sdk/credential/manifest"
+	errresp "github.com/TBD54566975/ssi-sdk/error"
 	"github.com/pkg/errors"
 
 	didint "github.com/tbd54566975/ssi-service/internal/did"
@@ -28,41 +30,66 @@ func (s Service) verifyApplicationJWT(did string, token keyaccess.JWT) error {
 }
 
 // validateCredentialApplication validates the credential application's signature(s) in addition to making sure it
-// is a valid credential application, and complies with its corresponding manifest
-// TODO: this should also (optionally) return the failed input descriptor(s)
-func (s Service) validateCredentialApplication(credManifest manifest.CredentialManifest, request SubmitApplicationRequest) error {
+// is a valid credential application, and complies with its corresponding manifest. it returns the ids of unfulfilled
+// input descriptors along with an error if validation fails.
+func (s Service) validateCredentialApplication(credManifest manifest.CredentialManifest, request SubmitApplicationRequest) (inputDescriptorIDs []string, err error) {
 	// validate the payload's signature
-	if err := s.verifyApplicationJWT(request.ApplicantDID, request.ApplicationJWT); err != nil {
+	if verificationErr := s.verifyApplicationJWT(request.ApplicantDID, request.ApplicationJWT); verificationErr != nil {
 		errMsg := fmt.Sprintf("could not verify application<%s>'s signature", request.Application.ID)
-		return util.LoggingErrorMsg(err, errMsg)
+		err = util.LoggingErrorMsg(err, errMsg)
+		return
 	}
 
 	// validate the application
 	credApp := request.Application
-	if err := credApp.IsValid(); err != nil {
-		return util.LoggingErrorMsg(err, "application is not valid")
+	if credErr := credApp.IsValid(); credErr != nil {
+		err = util.LoggingErrorMsg(credErr, "application is not valid")
+		return
 	}
 
 	// next, validate that the credential(s) provided in the application are valid
-	if _, err := manifest.IsValidCredentialApplicationForManifest(credManifest, request.ApplicationJSON); err != nil {
+	unfulfilledInputDescriptorIDs, validationErr := manifest.IsValidCredentialApplicationForManifest(credManifest, request.ApplicationJSON)
+	if validationErr != nil {
+		resp := errresp.GetErrorResponse(validationErr)
+		// a valid error response means this is an application level error, and we should return a credential denial
+		if resp.Valid {
+			if len(unfulfilledInputDescriptorIDs) > 0 {
+				var reasons []string
+				for id, reason := range unfulfilledInputDescriptorIDs {
+					inputDescriptorIDs = append(inputDescriptorIDs, id)
+					reasons = append(reasons, fmt.Sprintf("%s: %s", id, reason))
+				}
+				err = errresp.NewErrorResponsef(DenialResponse, "unfilled input descriptor(s): %s", strings.Join(reasons, ", "))
+				return
+			}
+			err = errresp.NewErrorResponseWithError(DenialResponse, resp.Err)
+			return
+		}
+
+		// otherwise, we have an internal error and  set the error to the value of the errResp's error
 		errMsg := fmt.Sprintf("could not validate application: %s", credApp.ID)
-		return util.LoggingErrorMsg(err, errMsg)
+		err = util.LoggingErrorMsg(resp.Err, errMsg)
+		return
 	}
 
 	// signature and validity checks for each credential submitted with the application
 	for _, credentialContainer := range request.Credentials {
-		verificationResult, err := s.credential.VerifyCredential(credential.VerifyCredentialRequest{
+		verificationResult, verificationErr := s.credential.VerifyCredential(credential.VerifyCredentialRequest{
 			DataIntegrityCredential: credentialContainer.Credential,
 			CredentialJWT:           credentialContainer.CredentialJWT,
 		})
-		if err != nil {
+
+		if verificationErr != nil {
 			errMsg := fmt.Sprintf("could not verify credential: %s", credentialContainer.Credential.ID)
-			return util.LoggingNewError(errMsg)
+			err = util.LoggingNewError(errMsg)
+			return
 		}
+
 		if !verificationResult.Verified {
 			errMsg := fmt.Sprintf("submitted credential<%s> is not valid: %s", credentialContainer.Credential.ID, verificationResult.Reason)
-			return util.LoggingNewError(errMsg)
+			err = util.LoggingNewError(errMsg)
+			return
 		}
 	}
-	return nil
+	return
 }
diff --git a/pkg/service/manifest/response.go b/pkg/service/manifest/response.go
index 6325ff4..e75415a 100644
--- a/pkg/service/manifest/response.go
+++ b/pkg/service/manifest/response.go
@@ -5,6 +5,7 @@ import (
 
 	"github.com/TBD54566975/ssi-sdk/credential/exchange"
 	"github.com/TBD54566975/ssi-sdk/credential/manifest"
+	errresp "github.com/TBD54566975/ssi-sdk/error"
 
 	cred "github.com/tbd54566975/ssi-service/internal/credential"
 	"github.com/tbd54566975/ssi-service/internal/keyaccess"
@@ -13,6 +14,11 @@ import (
 	"github.com/tbd54566975/ssi-service/pkg/service/keystore"
 )
 
+const (
+	// DenialResponse is an error response corresponding to a credential denial
+	DenialResponse errresp.Type = "DenialResponse"
+)
+
 func (s Service) signCredentialResponseJWT(signingDID string, r CredentialResponseContainer) (*keyaccess.JWT, error) {
 	gotKey, err := s.keyStore.GetKey(keystore.GetKeyRequest{ID: signingDID})
 	if err != nil {
diff --git a/pkg/service/manifest/service.go b/pkg/service/manifest/service.go
index 8d66b0a..77fd782 100644
--- a/pkg/service/manifest/service.go
+++ b/pkg/service/manifest/service.go
@@ -5,6 +5,7 @@ import (
 
 	"github.com/TBD54566975/ssi-sdk/credential/manifest"
 	didsdk "github.com/TBD54566975/ssi-sdk/did"
+	errresp "github.com/TBD54566975/ssi-sdk/error"
 	sdkutil "github.com/TBD54566975/ssi-sdk/util"
 	"github.com/sirupsen/logrus"
 
@@ -221,12 +222,16 @@ func (s Service) ProcessApplicationSubmission(request SubmitApplicationRequest)
 	credManifest := gotManifest.Manifest
 
 	// validate the application
-	if err = s.validateCredentialApplication(gotManifest.Manifest, request); err != nil {
-		denialResp, err := buildDenialCredentialResponse(manifestID, applicationID, err.Error())
-		if err != nil {
-			return nil, util.LoggingErrorMsg(err, "could not build denial credential response")
+	if unfulfilledInputDescriptorIDs, validationErr := s.validateCredentialApplication(gotManifest.Manifest, request); validationErr != nil {
+		resp := errresp.GetErrorResponse(validationErr)
+		if resp.ErrorType == DenialResponse {
+			denialResp, err := buildDenialCredentialResponse(manifestID, applicationID, resp.Err.Error(), unfulfilledInputDescriptorIDs...)
+			if err != nil {
+				return nil, util.LoggingErrorMsg(err, "could not build denial credential response")
+			}
+			return &SubmitApplicationResponse{Response: *denialResp}, nil
 		}
-		return &SubmitApplicationResponse{Response: *denialResp}, nil
+		return nil, util.LoggingErrorMsg(validationErr, "could not validate application")
 	}
 
 	// store the application


Test patch:
diff --git a/pkg/server/router/manifest_test.go b/pkg/server/router/manifest_test.go
index 228d22b..77ce9c0 100644
--- a/pkg/server/router/manifest_test.go
+++ b/pkg/server/router/manifest_test.go
@@ -137,6 +137,8 @@ func TestManifestRouter(t *testing.T) {
 		assert.NoError(tt, err)
 		assert.NotEmpty(tt, createdManifest)
 		assert.NotEmpty(tt, createdApplicationResponse.Response.ID)
+		assert.NotEmpty(tt, createdApplicationResponse.Response.Fulfillment)
+		assert.Empty(tt, createdApplicationResponse.Response.Denial)
 		assert.Equal(tt, len(createManifestRequest.OutputDescriptors), len(createdApplicationResponse.Credentials))
 	})
 }
diff --git a/pkg/server/server_manifest_test.go b/pkg/server/server_manifest_test.go
index 27d9445..1a998dd 100644
--- a/pkg/server/server_manifest_test.go
+++ b/pkg/server/server_manifest_test.go
@@ -408,6 +408,8 @@ func TestManifestAPI(t *testing.T) {
 		// good request
 		createManifestRequest := getValidManifestRequest(issuerDID.DID.ID, createdSchema.ID)
 
+		w.Flush()
+
 		requestValue := newRequestValue(tt, createManifestRequest)
 		req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests", requestValue)
 		err = manifestRouter.CreateManifest(newRequestContext(), w, req)
@@ -436,9 +438,12 @@ func TestManifestAPI(t *testing.T) {
 		signed, err := signer.SignJSON(applicationRequest)
 		assert.NoError(tt, err)
 
+		w.Flush()
+
 		applicationRequestValue := newRequestValue(tt, router.SubmitApplicationRequest{ApplicationJWT: *signed})
 		req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests/applications", applicationRequestValue)
 		err = manifestRouter.SubmitApplication(newRequestContext(), w, req)
+		assert.NoError(tt, err)
 
 		var appResp router.SubmitApplicationResponse
 		err = json.NewDecoder(w.Body).Decode(&appResp)
@@ -446,6 +451,162 @@ func TestManifestAPI(t *testing.T) {
 
 		assert.NotEmpty(tt, appResp.Response)
 		assert.Equal(tt, resp.Manifest.ID, appResp.Response.ManifestID)
+		assert.NotEmpty(tt, appResp.Response.Fulfillment)
+		assert.Len(tt, appResp.Response.Fulfillment.DescriptorMap, 2)
+		assert.Len(tt, appResp.Credentials, 2)
+		assert.Empty(tt, appResp.Response.Denial)
+	})
+
+	t.Run("Test Denied Application", func(tt *testing.T) {
+		bolt, err := storage.NewBoltDB()
+		require.NoError(tt, err)
+
+		// remove the db file after the test
+		tt.Cleanup(func() {
+			_ = bolt.Close()
+			_ = os.Remove(storage.DBFile)
+		})
+
+		keyStoreService := testKeyStoreService(tt, bolt)
+		didService := testDIDService(tt, bolt, keyStoreService)
+		schemaService := testSchemaService(tt, bolt, keyStoreService, didService)
+		credentialService := testCredentialService(tt, bolt, keyStoreService, didService, schemaService)
+		manifestRouter, _ := testManifest(tt, bolt, keyStoreService, didService, credentialService)
+
+		// missing required field: Application
+		badManifestRequest := router.SubmitApplicationRequest{
+			ApplicationJWT: "bad",
+		}
+
+		badRequestValue := newRequestValue(tt, badManifestRequest)
+		req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests/applications", badRequestValue)
+		w := httptest.NewRecorder()
+
+		err = manifestRouter.SubmitApplication(newRequestContext(), w, req)
+		assert.Error(tt, err)
+		assert.Contains(tt, err.Error(), "invalid submit application request")
+
+		// reset the http recorder
+		w.Flush()
+
+		// create an issuer
+		issuerDID, err := didService.CreateDIDByMethod(did.CreateDIDRequest{
+			Method:  didsdk.KeyMethod,
+			KeyType: crypto.Ed25519,
+		})
+		assert.NoError(tt, err)
+		assert.NotEmpty(tt, issuerDID)
+
+		// create an applicant
+		applicantDID, err := didService.CreateDIDByMethod(did.CreateDIDRequest{
+			Method:  didsdk.KeyMethod,
+			KeyType: crypto.Ed25519,
+		})
+		assert.NoError(tt, err)
+		assert.NotEmpty(tt, issuerDID)
+
+		// create a schema for the creds to be issued against
+		licenseSchema := map[string]interface{}{
+			"type": "object",
+			"properties": map[string]interface{}{
+				"licenseType": map[string]interface{}{
+					"type": "string",
+				},
+			},
+			"additionalProperties": true,
+		}
+		createdSchema, err := schemaService.CreateSchema(schema.CreateSchemaRequest{Author: issuerDID.DID.ID, Name: "license schema", Schema: licenseSchema, Sign: true})
+		assert.NoError(tt, err)
+		assert.NotEmpty(tt, createdSchema)
+
+		// issue a credential against the schema to the subject, from the issuer
+		createdCred, err := credentialService.CreateCredential(credential.CreateCredentialRequest{
+			Issuer:     issuerDID.DID.ID,
+			Subject:    applicantDID.DID.ID,
+			JSONSchema: createdSchema.ID,
+			Data:       map[string]interface{}{"licenseType": "WA-DL-CLASS-A"},
+		})
+		assert.NoError(tt, err)
+		assert.NotEmpty(tt, createdCred)
+
+		w.Flush()
+
+		// good request
+		createManifestRequest := getValidManifestRequest(issuerDID.DID.ID, createdSchema.ID)
+
+		requestValue := newRequestValue(tt, createManifestRequest)
+		req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests", requestValue)
+		err = manifestRouter.CreateManifest(newRequestContext(), w, req)
+		assert.NoError(tt, err)
+
+		var resp router.CreateManifestResponse
+		err = json.NewDecoder(w.Body).Decode(&resp)
+		assert.NoError(tt, err)
+
+		m := resp.Manifest
+		assert.NotEmpty(tt, m)
+		assert.Equal(tt, m.Issuer.ID, issuerDID.DID.ID)
+
+		// good application request
+		container := []credmodel.Container{{CredentialJWT: createdCred.CredentialJWT}}
+		applicationRequest := getValidApplicationRequest(applicantDID.DID.ID, m.ID, m.PresentationDefinition.ID,
+			m.PresentationDefinition.InputDescriptors[0].ID, container)
+
+		// remove the presentation to make this a bad request
+		savedSubmission := applicationRequest.CredentialApplication.PresentationSubmission
+		applicationRequest.CredentialApplication.PresentationSubmission = nil
+
+		// sign application
+		applicantPrivKeyBytes, err := base58.Decode(applicantDID.PrivateKeyBase58)
+		assert.NoError(tt, err)
+		applicantPrivKey, err := crypto.BytesToPrivKey(applicantPrivKeyBytes, applicantDID.KeyType)
+		assert.NoError(tt, err)
+		signer, err := keyaccess.NewJWKKeyAccess(applicantDID.DID.ID, applicantPrivKey)
+		assert.NoError(tt, err)
+		signed, err := signer.SignJSON(applicationRequest)
+		assert.NoError(tt, err)
+
+		w.Flush()
+
+		applicationRequestValue := newRequestValue(tt, router.SubmitApplicationRequest{ApplicationJWT: *signed})
+		req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests/applications", applicationRequestValue)
+		err = manifestRouter.SubmitApplication(newRequestContext(), w, req)
+		assert.NoError(tt, err)
+
+		var appResp router.SubmitApplicationResponse
+		err = json.NewDecoder(w.Body).Decode(&appResp)
+		assert.NoError(tt, err)
+
+		assert.NotEmpty(tt, appResp.Response)
+		assert.Equal(tt, resp.Manifest.ID, appResp.Response.ManifestID)
+		assert.NotEmpty(tt, appResp.Response.Denial)
+		assert.Contains(tt, appResp.Response.Denial.Reason, "no descriptors provided for application")
+		assert.Len(tt, appResp.Response.Denial.InputDescriptors, 0)
+
+		// submit it again, with an unfulfilled descriptor
+		savedSubmission.DescriptorMap[0].ID = "bad"
+		applicationRequest.CredentialApplication.PresentationSubmission = savedSubmission
+
+		w.Flush()
+
+		// sign application
+		signed, err = signer.SignJSON(applicationRequest)
+		assert.NoError(tt, err)
+
+		applicationRequestValue = newRequestValue(tt, router.SubmitApplicationRequest{ApplicationJWT: *signed})
+		req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests/applications", applicationRequestValue)
+		err = manifestRouter.SubmitApplication(newRequestContext(), w, req)
+		assert.NoError(tt, err)
+
+		err = json.NewDecoder(w.Body).Decode(&appResp)
+		assert.NoError(tt, err)
+
+		assert.NotEmpty(tt, appResp.Response)
+		assert.Equal(tt, resp.Manifest.ID, appResp.Response.ManifestID)
+		assert.NotEmpty(tt, appResp.Response.Denial)
+		assert.Contains(tt, appResp.Response.Denial.Reason, "unfilled input descriptor(s): test-id: no submission descriptor found for input descriptor")
+		assert.Len(tt, appResp.Response.Denial.InputDescriptors, 1)
+		assert.Equal(tt, appResp.Response.Denial.InputDescriptors[0], "test-id")
 	})
 
 	t.Run("Test Get Application By ID and Get Applications", func(tt *testing.T) {