Repository: TBD54566975/ssi-service
Pull request #592 (2023-07-13 16:01:37)

Added DID Configuration Verification Endpoint
# Overview
This PR adds the `v1/did-configurations/verification` endpoint, with documentation. It follows the spec in https://identity.foundation/.well-known/resources/did-configuration/#did-configuration-resource-verification

# Description
Now that we have an endpoint that can create a DIDConfiguration, this PR adds another endpoint so that clients can verify they've done the correct steps when hosting it (or that any other DID Configuration is correct).

# How Has This Been Tested?
Unit tests were added. Integration tests were not added, since verifying `https://www.tbd.website` fails with the following reason : `verifying JWT credential: error getting key to verify credential<>: did<did:key:z6Mkh4A6QTNCAFi4NZfnWt8qNSGXDGnNXhxXWewcpBrzS19v> has no verification methods with kid: did:key:z6Mkh4A6QTNCAFi4NZfnWt8qNSGXDGnNXhxXWewcpBrzS19v`. I.e. we need to update the JWT to use the verification method ID, instead of the DID. This is tracked in a separate issue. 

# Checklist


## References
https://identity.foundation/.well-known/resources/did-configuration/#did-configuration-resource-verification


codecov-commenter: ## [Codecov](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/592?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) Report
> Merging [#592](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/592?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) (ac348b0) into [main](https://app.codecov.io/gh/TBD54566975/ssi-service/commit/6e978ce8750a19c0140c5176c8cffa1e93b9e561?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) (6e978ce) will **decrease** coverage by `0.07%`.
> The diff coverage is `13.04%`.

```diff
@@            Coverage Diff             @@
##             main     #592      +/-   ##
==========================================
- Coverage   24.05%   23.98%   -0.07%     
==========================================
  Files          45       45              
  Lines        5330     5349      +19     
==========================================
+ Hits         1282     1283       +1     
- Misses       3807     3825      +18     
  Partials      241      241              
```


| [Impacted Files](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/592?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) | Coverage Δ | |
|---|---|---|
| [pkg/server/router/did\_configuration.go](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/592?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-cGtnL3NlcnZlci9yb3V0ZXIvZGlkX2NvbmZpZ3VyYXRpb24uZ28=) | `0.00% <0.00%> (ø)` | |
| [pkg/server/server.go](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/592?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-cGtnL3NlcnZlci9zZXJ2ZXIuZ28=) | `61.24% <100.00%> (+0.15%)` | :arrow_up: |



Patch:
diff --git a/doc/swagger.yaml b/doc/swagger.yaml
index 737519b..ffbbce4 100644
--- a/doc/swagger.yaml
+++ b/doc/swagger.yaml
@@ -851,6 +851,30 @@ definitions:
     - urls
     - verb
     type: object
+  github_com_tbd54566975_ssi-service_pkg_service_well-known.VerifyDIDConfigurationRequest:
+    properties:
+      origin:
+        description: |-
+          Represents an origin to fetch the DID Configuration Resource from. Must be serialized as described in https://html.spec.whatwg.org/multipage/browsers.html#origin.
+          The `scheme` MUST be `https`.
+        example: https://example.com
+        type: string
+    required:
+    - origin
+    type: object
+  github_com_tbd54566975_ssi-service_pkg_service_well-known.VerifyDIDConfigurationResponse:
+    properties:
+      didConfiguration:
+        description: The configuration that was fetched from the origin's well known
+          location.
+        type: string
+      reason:
+        description: When Verified == false, the reason why it wasn't verified.
+        type: string
+      verified:
+        description: Whether the DIDConfiguration was verified.
+        type: boolean
+    type: object
   jwx.PublicKeyJWK:
     properties:
       alg:
@@ -2522,6 +2546,36 @@ paths:
       summary: Create DIDConfiguration
       tags:
       - DIDConfigurationAPI
+  /v1/did-configurations/verification:
+    put:
+      consumes:
+      - application/json
+      description: Verifies a DID Configuration Resource according to https://identity.foundation/.well-known/resources/did-configuration/#did-configuration-resource-verification
+      parameters:
+      - description: request body
+        in: body
+        name: request
+        required: true
+        schema:
+          $ref: '#/definitions/github_com_tbd54566975_ssi-service_pkg_service_well-known.VerifyDIDConfigurationRequest'
+      produces:
+      - application/json
+      responses:
+        "201":
+          description: Created
+          schema:
+            $ref: '#/definitions/github_com_tbd54566975_ssi-service_pkg_service_well-known.VerifyDIDConfigurationResponse'
+        "400":
+          description: Bad request
+          schema:
+            type: string
+        "500":
+          description: Internal server error
+          schema:
+            type: string
+      summary: Verifies a DID Configuration Resource
+      tags:
+      - DIDConfigurationAPI
   /v1/dids:
     get:
       consumes:
diff --git a/internal/credential/verification.go b/internal/credential/verification.go
index d095324..27ce166 100644
--- a/internal/credential/verification.go
+++ b/internal/credential/verification.go
@@ -61,6 +61,20 @@ func (v Validator) VerifyJWTCredential(ctx context.Context, token keyaccess.JWT)
 	return v.staticValidationChecks(ctx, *cred)
 }
 
+func (v Validator) Verify(ctx context.Context, credential Container) error {
+	if credential.HasJWTCredential() {
+		err := v.VerifyJWTCredential(ctx, *credential.CredentialJWT)
+		if err != nil {
+			return err
+		}
+	} else {
+		if err := v.VerifyDataIntegrityCredential(ctx, *credential.Credential); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
 // VerifyDataIntegrityCredential first checks the signature on the given data integrity credential. Next, it runs
 // a set of static verification checks on the credential as per the credential service's configuration.
 func (v Validator) VerifyDataIntegrityCredential(ctx context.Context, credential credsdk.VerifiableCredential) error {
diff --git a/pkg/server/router/did_configuration.go b/pkg/server/router/did_configuration.go
index 94ce8e8..d982438 100644
--- a/pkg/server/router/did_configuration.go
+++ b/pkg/server/router/did_configuration.go
@@ -11,7 +11,7 @@ import (
 )
 
 type DIDConfigurationRouter struct {
-	service *wellknown.DIDConfigurationService
+	Service *wellknown.DIDConfigurationService
 }
 
 type CreateDIDConfigurationRequest struct {
@@ -54,6 +54,7 @@ type DIDConfiguration struct {
 	Context    any   `json:"@context" validate:"required"`
 	LinkedDIDs []any `json:"linked_dids" validate:"required"`
 }
+
 type CreateDIDConfigurationResponse struct {
 	// The location in which the `didConfiguration` value should be hosted.
 	WellKnownLocation string `json:"wellKnownLocation"`
@@ -90,7 +91,7 @@ func (wr DIDConfigurationRouter) CreateDIDConfiguration(c *gin.Context) {
 
 	req := request.toServiceRequest()
 
-	createDIDConfigurationResponse, err := wr.service.CreateDIDConfiguration(c, req)
+	createDIDConfigurationResponse, err := wr.Service.CreateDIDConfiguration(c, req)
 	if err != nil {
 		errMsg := "could not create did configuration"
 		framework.LoggingRespondErrWithMsg(c, err, errMsg, http.StatusInternalServerError)
@@ -108,6 +109,39 @@ func (wr DIDConfigurationRouter) CreateDIDConfiguration(c *gin.Context) {
 	return
 }
 
+// VerifyDIDConfiguration godoc
+//
+//	@Summary		Verifies a DID Configuration Resource
+//	@Description	Verifies a DID Configuration Resource according to https://identity.foundation/.well-known/resources/did-configuration/#did-configuration-resource-verification
+//	@Tags			DIDConfigurationAPI
+//	@Accept			json
+//	@Produce		json
+//	@Param			request	body		wellknown.VerifyDIDConfigurationRequest	true	"request body"
+//	@Success		201		{object}	wellknown.VerifyDIDConfigurationResponse
+//	@Failure		400		{string}	string	"Bad request"
+//	@Failure		500		{string}	string	"Internal server error"
+//	@Router			/v1/did-configurations/verification [put]
+func (wr DIDConfigurationRouter) VerifyDIDConfiguration(c *gin.Context) {
+	var request wellknown.VerifyDIDConfigurationRequest
+	invalidRequest := "invalid verify did configuration resource request"
+	if err := framework.Decode(c.Request, &request); err != nil {
+		framework.LoggingRespondErrWithMsg(c, err, invalidRequest, http.StatusBadRequest)
+		return
+	}
+
+	if err := framework.ValidateRequest(request); err != nil {
+		framework.LoggingRespondErrWithMsg(c, err, invalidRequest, http.StatusBadRequest)
+		return
+	}
+
+	response, err := wr.Service.VerifyDIDConfiguration(c, &request)
+	if err != nil {
+		errMsg := "could not verify did configuration"
+		framework.LoggingRespondErrWithMsg(c, err, errMsg, http.StatusInternalServerError)
+		return
+	}
+	framework.Respond(c, response, http.StatusCreated)
+}
 func NewDIDConfigurationsRouter(svc svcframework.Service) (*DIDConfigurationRouter, error) {
-	return &DIDConfigurationRouter{service: svc.(*wellknown.DIDConfigurationService)}, nil
+	return &DIDConfigurationRouter{Service: svc.(*wellknown.DIDConfigurationService)}, nil
 }
diff --git a/pkg/server/server.go b/pkg/server/server.go
index 3b49be2..2187b36 100644
--- a/pkg/server/server.go
+++ b/pkg/server/server.go
@@ -365,13 +365,14 @@ func WebhookAPI(rg *gin.RouterGroup, service svcframework.Service) (err error) {
 }
 
 func DIDConfigurationAPI(rg *gin.RouterGroup, service svcframework.Service) error {
-	webhookRouter, err := router.NewDIDConfigurationsRouter(service)
+	didConfigurationsRouter, err := router.NewDIDConfigurationsRouter(service)
 	if err != nil {
 		return sdkutil.LoggingErrorMsg(err, "creating webhook router")
 	}
 
 	webhookAPI := rg.Group(DIDConfigurationsPrefix)
-	webhookAPI.PUT("", webhookRouter.CreateDIDConfiguration)
+	webhookAPI.PUT("", didConfigurationsRouter.CreateDIDConfiguration)
+	webhookAPI.PUT(VerificationPath, didConfigurationsRouter.VerifyDIDConfiguration)
 
 	return nil
 }
diff --git a/pkg/service/service.go b/pkg/service/service.go
index 7460a42..a93e61e 100644
--- a/pkg/service/service.go
+++ b/pkg/service/service.go
@@ -145,7 +145,7 @@ func instantiateServices(config config.ServicesConfig) (*SSIService, error) {
 		return nil, sdkutil.LoggingErrorMsg(err, "could not instantiate the operation service")
 	}
 
-	didConfigurationService := wellknown.NewDIDConfigurationService(keyStoreService)
+	didConfigurationService, _ := wellknown.NewDIDConfigurationService(keyStoreService, didResolver, schemaService)
 	return &SSIService{
 		KeyStore:         keyStoreService,
 		DID:              didService,
diff --git a/pkg/service/well-known/did_configuration.go b/pkg/service/well-known/did_configuration.go
index d13e802..d5c13a3 100644
--- a/pkg/service/well-known/did_configuration.go
+++ b/pkg/service/well-known/did_configuration.go
@@ -1,24 +1,47 @@
 package wellknown
 
 import (
+	"bytes"
 	"context"
+	"fmt"
+	"io"
+	"net/http"
+	"strings"
 	"time"
 
 	"github.com/TBD54566975/ssi-sdk/credential"
 	"github.com/TBD54566975/ssi-sdk/credential/integrity"
 	"github.com/TBD54566975/ssi-sdk/did"
+	"github.com/TBD54566975/ssi-sdk/did/resolution"
+	"github.com/goccy/go-json"
 	"github.com/pkg/errors"
 	credint "github.com/tbd54566975/ssi-service/internal/credential"
+	"github.com/tbd54566975/ssi-service/internal/util"
 	svcframework "github.com/tbd54566975/ssi-service/pkg/service/framework"
 	"github.com/tbd54566975/ssi-service/pkg/service/keystore"
+	"github.com/tbd54566975/ssi-service/pkg/service/schema"
+	"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
 )
 
 type DIDConfigurationService struct {
 	keyStoreService *keystore.Service
+	validator       *credint.Validator
+
+	HTTPClient *http.Client
 }
 
-func NewDIDConfigurationService(keyStoreService *keystore.Service) *DIDConfigurationService {
-	return &DIDConfigurationService{keyStoreService: keyStoreService}
+func NewDIDConfigurationService(keyStoreService *keystore.Service, didResolver resolution.Resolver, schema *schema.Service) (*DIDConfigurationService, error) {
+	client := &http.Client{Transport: otelhttp.NewTransport(http.DefaultTransport)}
+	validator, err := credint.NewCredentialValidator(didResolver, schema)
+	if err != nil {
+		return nil, errors.Wrap(err, "could not instantiate validator for the credential service")
+	}
+
+	return &DIDConfigurationService{
+		keyStoreService: keyStoreService,
+		validator:       validator,
+		HTTPClient:      client,
+	}, nil
 }
 
 func (s DIDConfigurationService) Type() svcframework.Type {
@@ -33,6 +56,80 @@ func (s DIDConfigurationService) Status() svcframework.Status {
 
 var _ svcframework.Service = (*DIDConfigurationService)(nil)
 
+func (s DIDConfigurationService) VerifyDIDConfiguration(ctx context.Context, req *VerifyDIDConfigurationRequest) (*VerifyDIDConfigurationResponse, error) {
+	location := req.Origin + DIDConfigurationLocationSuffix
+	httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, location, nil)
+	if err != nil {
+		return nil, errors.Wrap(err, "creating http request")
+	}
+	if httpReq.URL.Scheme != "https" {
+		return nil, errors.Errorf("origin expected to be https but got %s", req.Origin)
+	}
+
+	httpResponse, err := s.HTTPClient.Do(httpReq)
+	defer func(Body io.ReadCloser) {
+		_ = Body.Close()
+	}(httpResponse.Body)
+	if err != nil {
+		return nil, errors.Wrap(err, "performing http request")
+	}
+
+	if !util.Is2xxResponse(httpResponse.StatusCode) {
+		return nil, errors.Errorf("expected 2xx code, got %d", httpResponse.StatusCode)
+	}
+
+	bodyCopy := new(bytes.Buffer)
+	bodyTeeReader := io.TeeReader(httpResponse.Body, bodyCopy)
+
+	var didConfiguration DIDConfiguration
+	if err := json.NewDecoder(bodyTeeReader).Decode(&didConfiguration); err != nil {
+		return nil, errors.Wrap(err, "decoding body")
+	}
+
+	response := VerifyDIDConfigurationResponse{
+		DIDConfiguration: bodyCopy.String(),
+	}
+	for _, domainLinkageCredential := range didConfiguration.LinkedDIDs {
+		// For a Domain Linkage Credential to be deemed valid, it MUST be successfully processed in accordance with the following steps:
+		//
+		// 1. The credentialSubject.id MUST be a DID, and the value MUST be equal to both the Subject and Issuer of the Domain Linkage Credential.
+		credentialSubjectID := domainLinkageCredential.Credential.CredentialSubject["id"].(string)
+		if !strings.HasPrefix(credentialSubjectID, "did:") {
+			response.Reason = fmt.Sprintf("The credentialSubject.id MUST be a DID, but got <%s>", credentialSubjectID)
+			return &response, nil
+		}
+
+		if credentialSubjectID != domainLinkageCredential.Credential.IssuerID() {
+			response.Reason = fmt.Sprintf("The credentialSubject.id MUST be equal to both the Subject and Issuer of the Domain Linkage Credential")
+			return &response, nil
+		}
+
+		// 2. The Domain Linkage Credential must be in either a Linked Data Proof Format or JSON Web Token Proof Format
+		if !domainLinkageCredential.HasSignedCredential() {
+			response.Reason = fmt.Sprintf("The Domain Linkage Credential must be in either a Linked Data Proof Format or JSON Web Token Proof Format")
+			return &response, nil
+		}
+
+		// 3. The credentialSubject.origin property MUST be present, and its value MUST match the origin the resource was requested from.
+		credentialSubjectOrigin := domainLinkageCredential.Credential.CredentialSubject["origin"].(string)
+		if !strings.HasPrefix(httpReq.URL.String(), credentialSubjectOrigin) {
+			response.Reason = fmt.Sprintf("The credentialSubject.origin property MUST be present, and its value MUST match the origin the resource was requested from")
+			return &response, nil
+		}
+
+		// 4. The implementer MUST perform DID resolution on the DID specified in the Issuer of the Domain Linkage Credential to obtain the associated DID document.
+		// 5. Using the retrieved DID document, the implementer MUST validate the signature of the Domain Linkage Credential against key material referenced in the assertionMethod section of the DID document.
+		if err := s.validator.Verify(ctx, domainLinkageCredential); err != nil {
+			response.Reason = err.Error()
+			return &response, nil
+		}
+
+		// 6. If Domain Linkage Credential verification is successfull, a Verifier SHOULD consider the entity controlling the origin and the Controller of the DID to be the same entity.
+	}
+
+	response.Verified = true
+	return &response, nil
+}
 func (s DIDConfigurationService) CreateDIDConfiguration(ctx context.Context, req *CreateDIDConfigurationRequest) (*CreateDIDConfigurationResponse, error) {
 	builder := credential.NewVerifiableCredentialBuilder()
 	if err := builder.SetIssuer(req.IssuerDID); err != nil {
diff --git a/pkg/service/well-known/model.go b/pkg/service/well-known/model.go
index c6f7be4..dc0d6fb 100644
--- a/pkg/service/well-known/model.go
+++ b/pkg/service/well-known/model.go
@@ -1,6 +1,10 @@
 package wellknown
 
-import "github.com/tbd54566975/ssi-service/internal/credential"
+import (
+	"github.com/goccy/go-json"
+	"github.com/pkg/errors"
+	"github.com/tbd54566975/ssi-service/internal/credential"
+)
 
 type CreateDIDConfigurationRequest struct {
 	IssuerDID            string
@@ -30,3 +34,39 @@ type DIDConfiguration struct {
 	Context    any                    `json:"@context" validate:"required"`
 	LinkedDIDs []credential.Container `json:"linked_dids" validate:"required"`
 }
+
+func (c *DIDConfiguration) UnmarshalJSON(data []byte) error {
+	var m map[string]any
+	if err := json.Unmarshal(data, &m); err != nil {
+		return err
+	}
+	c.Context = m["@context"]
+
+	linkedDIDs, ok := m["linked_dids"].([]any)
+	if !ok {
+		return errors.New("linked_dids expected to be array")
+	}
+	var err error
+	c.LinkedDIDs, err = credential.NewCredentialContainerFromArray(linkedDIDs)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+type VerifyDIDConfigurationRequest struct {
+	// Represents an origin to fetch the DID Configuration Resource from. Must be serialized as described in https://html.spec.whatwg.org/multipage/browsers.html#origin.
+	// The `scheme` MUST be `https`.
+	Origin string `json:"origin" validate:"required" example:"https://example.com"`
+}
+
+type VerifyDIDConfigurationResponse struct {
+	// Whether the DIDConfiguration was verified.
+	Verified bool `json:"verified"`
+
+	// The configuration that was fetched from the origin's well known location.
+	DIDConfiguration string `json:"didConfiguration"`
+
+	// When Verified == false, the reason why it wasn't verified.
+	Reason string `json:"reason,omitempty"`
+}


Test patch:
diff --git a/pkg/server/server_did_configuration_test.go b/pkg/server/server_did_configuration_test.go
index 94b0ed8..e0d0afe 100644
--- a/pkg/server/server_did_configuration_test.go
+++ b/pkg/server/server_did_configuration_test.go
@@ -7,29 +7,33 @@ import (
 	"testing"
 
 	"github.com/TBD54566975/ssi-sdk/credential/integrity"
+	"github.com/TBD54566975/ssi-sdk/did/resolution"
 	"github.com/goccy/go-json"
 	"github.com/stretchr/testify/assert"
 	"github.com/tbd54566975/ssi-service/internal/util"
 	"github.com/tbd54566975/ssi-service/pkg/server/router"
 	"github.com/tbd54566975/ssi-service/pkg/service/did"
 	"github.com/tbd54566975/ssi-service/pkg/service/keystore"
+	"github.com/tbd54566975/ssi-service/pkg/service/schema"
 	wellknown "github.com/tbd54566975/ssi-service/pkg/service/well-known"
 	"github.com/tbd54566975/ssi-service/pkg/testutil"
+	"gopkg.in/h2non/gock.v1"
 )
 
 func TestDIDConfigurationAPI(t *testing.T) {
-	t.Run("Create DID Configuration", func(tt *testing.T) {
+	t.Run("Create DID Configuration", func(t *testing.T) {
 		for _, test := range testutil.TestDatabases {
-			tt.Run(test.Name, func(ttt *testing.T) {
-				s := test.ServiceStorage(ttt)
+			t.Run(test.Name, func(t *testing.T) {
+				s := test.ServiceStorage(t)
 
-				keyStoreService, keyStoreServiceFactory := testKeyStoreService(ttt, s)
-				didService, _ := testDIDService(ttt, s, keyStoreService, keyStoreServiceFactory)
+				keyStoreService, keyStoreServiceFactory := testKeyStoreService(t, s)
+				didService, _ := testDIDService(t, s, keyStoreService, keyStoreServiceFactory)
+				schemaService := testSchemaService(t, s, keyStoreService, didService)
 
 				didKey, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{Method: "key", KeyType: "Ed25519"})
-				assert.NoError(ttt, err)
-				assert.NotEmpty(ttt, didKey)
-				dcRouter := setupDIDConfigurationRouter(ttt, keyStoreService)
+				assert.NoError(t, err)
+				assert.NotEmpty(t, didKey)
+				dcRouter := setupDIDConfigurationRouter(t, keyStoreService, didService.GetResolver(), schemaService)
 
 				request := router.CreateDIDConfigurationRequest{
 					IssuerDID:            didKey.DID.ID,
@@ -37,32 +41,130 @@ func TestDIDConfigurationAPI(t *testing.T) {
 					Origin:               "https://www.tbd.website/",
 					ExpirationDate:       "2051-10-05T14:48:00.000Z",
 				}
-				value := newRequestValue(ttt, request)
+				value := newRequestValue(t, request)
 				req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/did-configurations", value)
 				w := httptest.NewRecorder()
 				c := newRequestContext(w, req)
 				dcRouter.CreateDIDConfiguration(c)
 
-				assert.True(ttt, util.Is2xxResponse(w.Code))
+				assert.True(t, util.Is2xxResponse(w.Code))
 				var resp router.CreateDIDConfigurationResponse
-				assert.NoError(ttt, json.NewDecoder(w.Body).Decode(&resp))
+				assert.NoError(t, json.NewDecoder(w.Body).Decode(&resp))
 
 				_, _, vc, err := integrity.ParseVerifiableCredentialFromJWT(resp.DIDConfiguration.LinkedDIDs[0].(string))
-				assert.NoError(ttt, err)
-				assert.Equal(ttt, "DomainLinkageCredential", vc.Type.([]any)[1].(string))
-				assert.Equal(ttt, "https://www.tbd.website/", vc.CredentialSubject["origin"])
-				assert.Equal(ttt, didKey.DID.ID, vc.CredentialSubject["id"])
-				assert.NotEmpty(ttt, vc.IssuanceDate)
-				assert.NotEmpty(ttt, vc.ExpirationDate)
-				assert.Empty(ttt, vc.ID)
-				assert.Contains(ttt, resp.WellKnownLocation, "/.well-known/did-configuration.json")
+				assert.NoError(t, err)
+				assert.Equal(t, "DomainLinkageCredential", vc.Type.([]any)[1].(string))
+				assert.Equal(t, "https://www.tbd.website/", vc.CredentialSubject["origin"])
+				assert.Equal(t, didKey.DID.ID, vc.CredentialSubject["id"])
+				assert.NotEmpty(t, vc.IssuanceDate)
+				assert.NotEmpty(t, vc.ExpirationDate)
+				assert.Empty(t, vc.ID)
+				assert.Contains(t, resp.WellKnownLocation, "/.well-known/did-configuration.json")
+			})
+		}
+	})
+
+	t.Run("Verify DID Configuration", func(t *testing.T) {
+		for _, test := range testutil.TestDatabases {
+			t.Run(test.Name, func(t *testing.T) {
+				s := test.ServiceStorage(t)
+
+				keyStoreService, keyStoreServiceFactory := testKeyStoreService(t, s)
+				didService, _ := testDIDService(t, s, keyStoreService, keyStoreServiceFactory)
+				schemaService := testSchemaService(t, s, keyStoreService, didService)
+
+				didKey, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{Method: "key", KeyType: "Ed25519"})
+				assert.NoError(t, err)
+				assert.NotEmpty(t, didKey)
+
+				didConfigurationService := setupDIDConfigurationRouter(t, keyStoreService, didService.GetResolver(), schemaService)
+
+				t.Run("passes for TBD", func(t *testing.T) {
+					defer gock.Off()
+
+					gock.InterceptClient(didConfigurationService.Service.HTTPClient)
+					gock.New("https://www.tbd.website").
+						Get("/.well-known/did-configuration.json").
+						Reply(200).
+						BodyString(`{
+    "@context": "https://identity.foundation/.well-known/did-configuration/v1",
+    "linked_dids": [
+      "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa2hDcWNza2haU0V4NEZHbU5XNXNNcVVoZzJyZnc2YnMyWkNCZXBzTkZjQ3Y1I3o2TWtoQ3Fjc2toWlNFeDRGR21OVzVzTXFVaGcycmZ3NmJzMlpDQmVwc05GY0N2NSIsInR5cCI6IkpXVCJ9.eyJleHAiOjI1ODAxMzAwODAsImlhdCI6MTYzMzQ0NTI4MCwiaXNzIjoiZGlkOmtleTp6Nk1raENxY3NraFpTRXg0RkdtTlc1c01xVWhnMnJmdzZiczJaQ0JlcHNORmNDdjUiLCJuYmYiOjE2MzM0NDUyODAsIm5vbmNlIjoiOTU5MTczMDktYWJmMC00OWI0LTkzYTktMjQyYTg4NmFhYTBmIiwic3ViIjoiZGlkOmtleTp6Nk1raENxY3NraFpTRXg0RkdtTlc1c01xVWhnMnJmdzZiczJaQ0JlcHNORmNDdjUiLCJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vaWRlbnRpdHkuZm91bmRhdGlvbi8ud2VsbC1rbm93bi9kaWQtY29uZmlndXJhdGlvbi92MSJdLCJjcmVkZW50aWFsU3ViamVjdCI6eyJvcmlnaW4iOiJodHRwczovL3d3dy50YmQud2Vic2l0ZSJ9LCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiRG9tYWluTGlua2FnZUNyZWRlbnRpYWwiXX19.OV56DeG2bp4Hd-kulCUgZCk4PgY51sEzw4REZeUfiS4Cqa9LnbK0fkiJ2jPAZoBzTSDq75Hxc2qX5Ey0JVCoBw"
+    ]
+  }`)
+
+					request := wellknown.VerifyDIDConfigurationRequest{
+						Origin: "https://www.tbd.website/",
+					}
+					value := newRequestValue(t, request)
+					req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/did-configurations/verification", value)
+					w := httptest.NewRecorder()
+					c := newRequestContext(w, req)
+					didConfigurationService.VerifyDIDConfiguration(c)
+
+					var resp wellknown.VerifyDIDConfigurationResponse
+					err := json.NewDecoder(w.Body).Decode(&resp)
+					assert.NoError(t, err)
+
+					assert.True(t, resp.Verified, resp.Reason)
+					assert.NotEmpty(t, resp.DIDConfiguration)
+					assert.Empty(t, resp.Reason)
+				})
+
+				t.Run("verification fails", func(t *testing.T) {
+					defer gock.Off()
+
+					gock.InterceptClient(didConfigurationService.Service.HTTPClient)
+					gock.New("https://www.tbd.website").
+						Get("/.well-known/did-configuration.json").
+						Reply(200).
+						BodyString(`{
+    "@context":"https://identity.foundation/.well-known/did-configuration/v1",
+    "linked_dids":[
+       "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa2g0QTZRVE5DQUZpNE5aZm5XdDhxTlNHWERHbk5YaHhYV2V3Y3BCcnpTMTl2IiwidHlwIjoiSldUIn0.eyJleHAiOjI1ODAxMzAwODAsImlzcyI6ImRpZDprZXk6ejZNa2g0QTZRVE5DQUZpNE5aZm5XdDhxTlNHWERHbk5YaHhYV2V3Y3BCcnpTMTl2IiwibmJmIjoxNjc2NTcyMDkzLCJzdWIiOiJkaWQ6a2V5Ono2TWtoNEE2UVROQ0FGaTROWmZuV3Q4cU5TR1hER25OWGh4WFdld2NwQnJ6UzE5diIsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIiwiaHR0cHM6Ly9pZGVudGl0eS5mb3VuZGF0aW9uLy53ZWxsLWtub3duL2RpZC1jb25maWd1cmF0aW9uL3YxIl0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJEb21haW5MaW5rYWdlQ3JlZGVudGlhbCJdLCJpc3N1ZXIiOiJkaWQ6a2V5Ono2TWtoNEE2UVROQ0FGaTROWmZuV3Q4cU5TR1hER25OWGh4WFdld2NwQnJ6UzE5diIsImlzc3VhbmNlRGF0ZSI6IjIwMjMtMDItMTZUMTI6Mjg6MTMtMDY6MDAiLCJleHBpcmF0aW9uRGF0ZSI6IjIwNTEtMTAtMDVUMTQ6NDg6MDAuMDAwWiIsImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6Nk1raDRBNlFUTkNBRmk0Tlpmbld0OHFOU0dYREduTlhoeFhXZXdjcEJyelMxOXYiLCJvcmlnaW4iOiJodHRwczovL3d3dy50YmQud2Vic2l0ZS8ifX19.szn9o_JhCLqYMH_SNtwFaJWViueg-pvrZW4G88cegh2Airh9ziQ7fYvSY4Hts2FlF6at8fMfAzrsnhJ-Fb0_Dw",
+       "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6dGJkLndlYnNpdGUiLCJ0eXAiOiJKV1QifQ.eyJleHAiOjI1ODAxMzAwODAsImlzcyI6ImRpZDp3ZWI6dGJkLndlYnNpdGUiLCJuYmYiOjE2NzY1NzIwOTUsInN1YiI6ImRpZDp3ZWI6dGJkLndlYnNpdGUiLCJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vaWRlbnRpdHkuZm91bmRhdGlvbi8ud2VsbC1rbm93bi9kaWQtY29uZmlndXJhdGlvbi92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiRG9tYWluTGlua2FnZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOndlYjp0YmQud2Vic2l0ZSIsImlzc3VhbmNlRGF0ZSI6IjIwMjMtMDItMTZUMTI6Mjg6MTUtMDY6MDAiLCJleHBpcmF0aW9uRGF0ZSI6IjIwNTEtMTAtMDVUMTQ6NDg6MDAuMDAwWiIsImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOndlYjp0YmQud2Vic2l0ZSIsIm9yaWdpbiI6Imh0dHBzOi8vd3d3LnRiZC53ZWJzaXRlLyJ9fX0.bweamOE6q-K1jQ64cfqk-vhhuugSpLvcit3Q6REBM2z0CpvvTX4SttHF533oUIDovtOSqmAAOOUFCbrTJYQfDw"
+    ]
+ }`)
+
+					request := wellknown.VerifyDIDConfigurationRequest{
+						Origin: "https://www.tbd.website/",
+					}
+					value := newRequestValue(t, request)
+					req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/did-configurations/verification", value)
+					w := httptest.NewRecorder()
+					c := newRequestContext(w, req)
+					didConfigurationService.VerifyDIDConfiguration(c)
+
+					var resp wellknown.VerifyDIDConfigurationResponse
+					err := json.NewDecoder(w.Body).Decode(&resp)
+					assert.NoError(t, err)
+
+					assert.False(t, resp.Verified)
+					assert.NotEmpty(t, resp.DIDConfiguration)
+					assert.Contains(t, resp.Reason, "verifying JWT credential")
+				})
+
+				t.Run("returns error for non tls", func(t *testing.T) {
+					request := wellknown.VerifyDIDConfigurationRequest{
+						Origin: "http://www.tbd.website/",
+					}
+					value := newRequestValue(t, request)
+					req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/did-configurations/verification", value)
+					w := httptest.NewRecorder()
+					c := newRequestContext(w, req)
+					didConfigurationService.VerifyDIDConfiguration(c)
+
+					assert.False(t, util.Is2xxResponse(w.Code))
+					assert.Contains(t, w.Body.String(), "origin expected to be https")
+				})
 			})
 		}
 	})
 }
 
-func setupDIDConfigurationRouter(t *testing.T, keyStoreService *keystore.Service) *router.DIDConfigurationRouter {
-	service := wellknown.NewDIDConfigurationService(keyStoreService)
+func setupDIDConfigurationRouter(t *testing.T, keyStoreService *keystore.Service, didResolver resolution.Resolver, schemaService *schema.Service) *router.DIDConfigurationRouter {
+	service, err := wellknown.NewDIDConfigurationService(keyStoreService, didResolver, schemaService)
+	assert.NoError(t, err)
 
 	dcRouter, err := router.NewDIDConfigurationsRouter(service)
 	assert.NoError(t, err)