Repository: TBD54566975/ssi-service
Pull request #314 (2023-03-08 22:28:10)

Adding support for resolving ION dids
# Overview
This adds universal resolver for DIDs that use `ion` did method.

# Description
I added the universal resolver docker container. It's called when `ResolveDID` endpoint is called with the registered universal resolver did method. 

Additionally, I changed all the piping so it can be used for verification. 

# How Has This Been Tested?
Locally. Integration tests don't pass because of networking snafu. 

# Checklist

Before submitting this PR, please make sure:

- [x] I have read the CONTRIBUTING document.
- [x] My code is consistent with the rest of the project 
- [x] I have tagged the relevant reviewers and/or interested parties
- [x] I have updated the READMEs and other documentation of affected packages

## References
See https://github.com/decentralized-identity/universal-resolver for how the universal resolver is set up. 

Seems this is also related to #237, though there is no caching here.

michaelneale: hey this is pretty neat! clever approach

https://hub.docker.com/r/universalregistrar/driver-did-ion/ - is that the image that should be use? (I see e2e failing).
codecov-commenter: # [Codecov](https://codecov.io/gh/TBD54566975/ssi-service/pull/314?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) Report
> Merging [#314](https://codecov.io/gh/TBD54566975/ssi-service/pull/314?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) (3a8a1c9) into [main](https://codecov.io/gh/TBD54566975/ssi-service/commit/a1e6968bc3edb4d4157e665db5beffacb42266f6?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) (a1e6968) will **not change** coverage.
> The diff coverage is `0.00%`.

> :exclamation: Current head 3a8a1c9 differs from pull request most recent head ce78b0c. Consider uploading reports for the commit ce78b0c to get more accurate results

```diff
@@           Coverage Diff           @@
##             main     #314   +/-   ##
=======================================
  Coverage   18.73%   18.73%           
=======================================
  Files          36       36           
  Lines        4292     4292           
=======================================
  Hits          804      804           
  Misses       3368     3368           
  Partials      120      120           
```


| [Impacted Files](https://codecov.io/gh/TBD54566975/ssi-service/pull/314?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) | Coverage Δ | |
|---|---|---|
| [config/config.go](https://codecov.io/gh/TBD54566975/ssi-service/pull/314?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-Y29uZmlnL2NvbmZpZy5nbw==) | `16.14% <ø> (ø)` | |
| [internal/did/access.go](https://codecov.io/gh/TBD54566975/ssi-service/pull/314?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-aW50ZXJuYWwvZGlkL2FjY2Vzcy5nbw==) | `19.35% <0.00%> (ø)` | |
| [pkg/service/credential/service.go](https://codecov.io/gh/TBD54566975/ssi-service/pull/314?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%> (ø)` | |

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)

andresuribe87: @michaelneale the image problem is fixed now. PTAL
michaelneale: looks good - but I am probably not best reviewier for this.

Patch:
diff --git a/.github/workflows/steelthread.yml b/.github/workflows/steelthread.yml
index 616473c..8b75e54 100644
--- a/.github/workflows/steelthread.yml
+++ b/.github/workflows/steelthread.yml
@@ -27,7 +27,7 @@ jobs:
       run: go install github.com/magefile/mage
 
     - name: Start containers
-      run: docker-compose -f "build/docker-compose.yml" up -d --build web redis
+      run: docker-compose -f "build/docker-compose.yml" up -d --build web redis uni-resolver-web driver-did-ion
 
     - name: End To End Test
       run: mage integration
diff --git a/build/docker-compose.yml b/build/docker-compose.yml
index 00bd573..ba0d4d2 100644
--- a/build/docker-compose.yml
+++ b/build/docker-compose.yml
@@ -17,6 +17,9 @@ services:
       - ../config/compose.toml:/app/config/config.toml
     networks:
       - ssi_network
+      - universal-resolver
+    links:
+      - uni-resolver-web
   swagger-ui:
     build:
       context: ../
@@ -63,6 +66,21 @@ services:
       - "9001:8081"
     networks:
       - ssi_network
+  uni-resolver-web:
+    image: universalresolver/uni-resolver-web:latest
+    ports:
+      - "8088:8080"
+    platform: "linux/amd64"
+    networks:
+      - universal-resolver
+  driver-did-ion:
+    image: identityfoundation/driver-did-ion:v0.8.1
+    ports:
+      - "8089:8080"
+    platform: "linux/amd64"
+    networks:
+      - universal-resolver
 
 networks:
-  ssi_network:
\ No newline at end of file
+  ssi_network:
+  universal-resolver:
\ No newline at end of file
diff --git a/config/compose.toml b/config/compose.toml
index d24af3d..6e2a7ac 100644
--- a/config/compose.toml
+++ b/config/compose.toml
@@ -40,6 +40,8 @@ password = "default-password"
 name = "did"
 methods = ["key", "web"]
 resolution_methods = ["key", "web", "pkh", "peer"]
+universal_resolver_url = "http://uni-resolver-web:8080"
+universal_resolver_methods = ["ion"]
 
 [services.schema]
 name = "schema"
diff --git a/config/config.go b/config/config.go
index 9061580..c94e2f7 100644
--- a/config/config.go
+++ b/config/config.go
@@ -104,8 +104,10 @@ func (k *KeyStoreServiceConfig) IsEmpty() bool {
 
 type DIDServiceConfig struct {
 	*BaseServiceConfig
-	Methods           []string `toml:"methods"`
-	ResolutionMethods []string `toml:"resolution_methods"`
+	Methods                  []string `toml:"methods"`
+	ResolutionMethods        []string `toml:"resolution_methods"`
+	UniversalResolverURL     string   `toml:"universal_resolver_url"`
+	UniversalResolverMethods []string `toml:"universal_resolver_methods"`
 }
 
 func (d *DIDServiceConfig) IsEmpty() bool {
diff --git a/config/config.toml b/config/config.toml
index 5da7bbc..1f47757 100644
--- a/config/config.toml
+++ b/config/config.toml
@@ -38,6 +38,8 @@ password = "default-password"
 name = "did"
 methods = ["key", "web"]
 resolution_methods = ["key", "web", "pkh", "peer"]
+universal_resolver_url = "http://localhost:8088"
+universal_resolver_methods = ["ion"]
 
 [services.schema]
 name = "schema"
diff --git a/internal/credential/verification.go b/internal/credential/verification.go
index 07bf6eb..78bee39 100644
--- a/internal/credential/verification.go
+++ b/internal/credential/verification.go
@@ -8,9 +8,9 @@ import (
 	credsdk "github.com/TBD54566975/ssi-sdk/credential"
 	"github.com/TBD54566975/ssi-sdk/credential/signing"
 	"github.com/TBD54566975/ssi-sdk/credential/verification"
-	didsdk "github.com/TBD54566975/ssi-sdk/did"
 	"github.com/goccy/go-json"
 	"github.com/pkg/errors"
+	"github.com/tbd54566975/ssi-service/pkg/service/did/resolve"
 
 	didint "github.com/tbd54566975/ssi-service/internal/did"
 	"github.com/tbd54566975/ssi-service/internal/keyaccess"
@@ -20,13 +20,13 @@ import (
 
 type Verifier struct {
 	verifier       *verification.CredentialVerifier
-	didResolver    *didsdk.Resolver
+	didResolver    resolve.Resolver
 	schemaResolver schema.Resolution
 }
 
 // NewCredentialVerifier creates a new credential verifier which executes both signature and static verification checks.
 // In the future the set of verification checks will be configurable.
-func NewCredentialVerifier(didResolver *didsdk.Resolver, schemaResolver schema.Resolution) (*Verifier, error) {
+func NewCredentialVerifier(didResolver resolve.Resolver, schemaResolver schema.Resolution) (*Verifier, error) {
 	if didResolver == nil {
 		return nil, errors.New("didResolver cannot be nil")
 	}
@@ -64,7 +64,7 @@ func (v Verifier) VerifyJWTCredential(ctx context.Context, token keyaccess.JWT)
 	}
 
 	// resolve the issuer's key material
-	kid, pubKey, err := v.resolveCredentialIssuerKey(*cred)
+	kid, pubKey, err := v.resolveCredentialIssuerKey(ctx, *cred)
 	if err != nil {
 		return util.LoggingError(err)
 	}
@@ -92,7 +92,7 @@ func (v Verifier) VerifyJWTCredential(ctx context.Context, token keyaccess.JWT)
 // a set of static verification checks on the credential as per the credential service's configuration.
 func (v Verifier) VerifyDataIntegrityCredential(ctx context.Context, credential credsdk.VerifiableCredential) error {
 	// resolve the issuer's key material
-	kid, pubKey, err := v.resolveCredentialIssuerKey(credential)
+	kid, pubKey, err := v.resolveCredentialIssuerKey(ctx, credential)
 	if err != nil {
 		return util.LoggingError(err)
 	}
@@ -112,9 +112,9 @@ func (v Verifier) VerifyDataIntegrityCredential(ctx context.Context, credential
 	return v.staticVerificationChecks(ctx, credential)
 }
 
-func (v Verifier) VerifyJWT(did string, token keyaccess.JWT) error {
+func (v Verifier) VerifyJWT(ctx context.Context, did string, token keyaccess.JWT) error {
 	// resolve the did's key material
-	kid, pubKey, err := didint.ResolveKeyForDID(v.didResolver, did)
+	kid, pubKey, err := didint.ResolveKeyForDID(ctx, v.didResolver, did)
 	if err != nil {
 		return util.LoggingError(err)
 	}
@@ -137,9 +137,9 @@ func (v Verifier) VerifyJWT(did string, token keyaccess.JWT) error {
 // resolveCredentialIssuerKey resolves the issuer's public key from the credential's issuer DID.
 // TODO(gabe): perhaps this should be a verification method referenced on the proof object, not the issuer
 // TODO(gabe): support issuers that are not strings, but objects
-func (v Verifier) resolveCredentialIssuerKey(credential credsdk.VerifiableCredential) (kid string, pubKey crypto.PublicKey, err error) {
+func (v Verifier) resolveCredentialIssuerKey(ctx context.Context, credential credsdk.VerifiableCredential) (kid string, pubKey crypto.PublicKey, err error) {
 	issuerDID := credential.Issuer.(string)
-	return didint.ResolveKeyForDID(v.didResolver, issuerDID)
+	return didint.ResolveKeyForDID(ctx, v.didResolver, issuerDID)
 
 }
 
diff --git a/internal/did/access.go b/internal/did/access.go
index cfb54f5..417ca32 100644
--- a/internal/did/access.go
+++ b/internal/did/access.go
@@ -1,6 +1,7 @@
 package did
 
 import (
+	"context"
 	"crypto"
 	"fmt"
 
@@ -13,6 +14,7 @@ import (
 	"github.com/multiformats/go-varint"
 	"github.com/pkg/errors"
 	"github.com/sirupsen/logrus"
+	"github.com/tbd54566975/ssi-service/pkg/service/did/resolve"
 )
 
 // GetVerificationInformation resolves a DID and provides a kid and public key needed for data verification
@@ -117,8 +119,8 @@ func multibaseToPubKeyBytes(mb string) ([]byte, error) {
 }
 
 // ResolveKeyForDID resolves a public key from a DID.
-func ResolveKeyForDID(resolver *didsdk.Resolver, did string) (kid string, pubKey crypto.PublicKey, err error) {
-	resolved, err := resolver.Resolve(did, nil)
+func ResolveKeyForDID(ctx context.Context, resolver resolve.Resolver, did string) (kid string, pubKey crypto.PublicKey, err error) {
+	resolved, err := resolver.Resolve(ctx, did, nil)
 	if err != nil {
 		err = errors.Wrapf(err, "failed to resolve did: %s", did)
 		return
diff --git a/pkg/jwt/verification.go b/pkg/jwt/verification.go
index d8019a2..55baacc 100644
--- a/pkg/jwt/verification.go
+++ b/pkg/jwt/verification.go
@@ -1,19 +1,20 @@
 package jwt
 
 import (
-	didsdk "github.com/TBD54566975/ssi-sdk/did"
-	"github.com/pkg/errors"
+	"context"
 
+	"github.com/pkg/errors"
 	didint "github.com/tbd54566975/ssi-service/internal/did"
 	"github.com/tbd54566975/ssi-service/internal/keyaccess"
 	"github.com/tbd54566975/ssi-service/internal/util"
+	"github.com/tbd54566975/ssi-service/pkg/service/did/resolve"
 )
 
 // VerifyTokenFromDID verifies that the information in the token was digitally signed by the public key associated with
 // the public key of the verification method of the did's document. The passed in resolver is used to map from the did
 // to the did document.
-func VerifyTokenFromDID(did string, token keyaccess.JWT, s *didsdk.Resolver) error {
-	kid, pubKey, err := didint.ResolveKeyForDID(s, did)
+func VerifyTokenFromDID(ctx context.Context, did string, token keyaccess.JWT, s resolve.Resolver) error {
+	kid, pubKey, err := didint.ResolveKeyForDID(ctx, s, did)
 	if err != nil {
 		return errors.Wrapf(err, "failed to resolve applicant's did: %s", did)
 	}
diff --git a/pkg/server/router/schema.go b/pkg/server/router/schema.go
index e5c5125..f631699 100644
--- a/pkg/server/router/schema.go
+++ b/pkg/server/router/schema.go
@@ -94,7 +94,7 @@ func (sr SchemaRouter) CreateSchema(ctx context.Context, w http.ResponseWriter,
 // @Success     200 {object} GetSchemaResponse
 // @Failure     400 {string} string "Bad request"
 // @Router      /v1/schemas/{id} [get]
-func (sr SchemaRouter) GetSchema(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
+func (sr SchemaRouter) GetSchema(ctx context.Context, w http.ResponseWriter, _ *http.Request) error {
 	id := framework.GetParam(ctx, IDParam)
 	if id == nil {
 		errMsg := "cannot get schema without ID parameter"
@@ -128,7 +128,7 @@ type GetSchemasResponse struct {
 // @Success     200 {object} GetSchemasResponse
 // @Failure     500 {string} string "Internal server error"
 // @Router      /v1/schemas [get]
-func (sr SchemaRouter) GetSchemas(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
+func (sr SchemaRouter) GetSchemas(ctx context.Context, w http.ResponseWriter, _ *http.Request) error {
 	gotSchemas, err := sr.service.GetSchemas(ctx)
 	if err != nil {
 		errMsg := "could not get schemas"
@@ -178,7 +178,7 @@ func (sr SchemaRouter) VerifySchema(ctx context.Context, w http.ResponseWriter,
 		return framework.NewRequestError(errors.Wrap(err, errMsg), http.StatusBadRequest)
 	}
 
-	verificationResult, err := sr.service.VerifySchema(schema.VerifySchemaRequest{SchemaJWT: request.SchemaJWT})
+	verificationResult, err := sr.service.VerifySchema(context.Background(), schema.VerifySchemaRequest{SchemaJWT: request.SchemaJWT})
 	if err != nil {
 		errMsg := "could not verify schema"
 		logrus.WithError(err).Error(errMsg)
diff --git a/pkg/service/credential/service.go b/pkg/service/credential/service.go
index 23dc141..82cb0e6 100644
--- a/pkg/service/credential/service.go
+++ b/pkg/service/credential/service.go
@@ -9,16 +9,15 @@ import (
 	"github.com/TBD54566975/ssi-sdk/credential"
 	schemalib "github.com/TBD54566975/ssi-sdk/credential/schema"
 	statussdk "github.com/TBD54566975/ssi-sdk/credential/status"
-	didsdk "github.com/TBD54566975/ssi-sdk/did"
 	sdkutil "github.com/TBD54566975/ssi-sdk/util"
 	"github.com/google/uuid"
 	"github.com/pkg/errors"
 	"github.com/sirupsen/logrus"
-
 	"github.com/tbd54566975/ssi-service/config"
 	credint "github.com/tbd54566975/ssi-service/internal/credential"
 	"github.com/tbd54566975/ssi-service/internal/keyaccess"
 	"github.com/tbd54566975/ssi-service/internal/util"
+	"github.com/tbd54566975/ssi-service/pkg/service/did/resolve"
 	"github.com/tbd54566975/ssi-service/pkg/service/framework"
 	"github.com/tbd54566975/ssi-service/pkg/service/keystore"
 	"github.com/tbd54566975/ssi-service/pkg/service/schema"
@@ -66,7 +65,7 @@ func (s Service) Config() config.CredentialServiceConfig {
 	return s.config
 }
 
-func NewCredentialService(config config.CredentialServiceConfig, s storage.ServiceStorage, keyStore *keystore.Service, didResolver *didsdk.Resolver, schema *schema.Service) (*Service, error) {
+func NewCredentialService(config config.CredentialServiceConfig, s storage.ServiceStorage, keyStore *keystore.Service, didResolver resolve.Resolver, schema *schema.Service) (*Service, error) {
 	credentialStorage, err := NewCredentialStorage(s)
 	if err != nil {
 		return nil, util.LoggingErrorMsg(err, "could not instantiate storage for the credential service")
diff --git a/pkg/service/did/resolve/universal.go b/pkg/service/did/resolve/universal.go
new file mode 100644
index 0000000..7b9555e
--- /dev/null
+++ b/pkg/service/did/resolve/universal.go
@@ -0,0 +1,69 @@
+package resolve
+
+import (
+	"bufio"
+	"context"
+	"io"
+	"net/http"
+
+	didsdk "github.com/TBD54566975/ssi-sdk/did"
+	"github.com/goccy/go-json"
+	"github.com/pkg/errors"
+)
+
+// UniversalResolver is a struct that implements the Resolver interface. It calls the universal resolver endpoint
+// to resolve any DID according to https://github.com/decentralized-identity/universal-resolver.
+type UniversalResolver struct {
+	Client http.Client
+	URL    string
+}
+
+type Resolver interface {
+	Resolve(ctx context.Context, did string, opts ...didsdk.ResolutionOptions) (*didsdk.DIDResolutionResult, error)
+}
+
+// Resolve results resolution results by doing a GET on <URL>/1.0.identifiers/<did>.
+func (ur UniversalResolver) Resolve(ctx context.Context, did string, _ ...didsdk.ResolutionOptions) (*didsdk.DIDResolutionResult, error) {
+	url := ur.URL + "/1.0/identifiers/" + did
+	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
+	if err != nil {
+		return nil, errors.Wrap(err, "creating request")
+	}
+
+	resp, err := ur.Client.Do(req)
+	if err != nil {
+		return nil, errors.Wrap(err, "performing http get")
+	}
+
+	respBody, err := io.ReadAll(bufio.NewReader(resp.Body))
+	if err != nil {
+		return nil, err
+	}
+	var result ResolutionResult
+	if err := json.Unmarshal(respBody, &result); err != nil {
+		return nil, errors.Wrap(err, "unmarshalling JSON")
+	}
+	return &didsdk.DIDResolutionResult{
+		DIDResolutionMetadata: result.DIDResolutionMetadata,
+		DIDDocument:           result.DIDDocument,
+		DIDDocumentMetadata:   result.DIDDocumentMetadata,
+	}, nil
+}
+
+type ResolutionResult struct {
+	Context                      any `json:"@context"`
+	didsdk.DIDResolutionMetadata `json:"didResolutionMetadata"`
+	didsdk.DIDDocument           `json:"didDocument"`
+	didsdk.DIDDocumentMetadata   `json:"didDocumentMetadata"`
+}
+
+// LocalResolver is an implementation of Resolver that passes through the parameters into the sdk implementation that
+// resolves DIDs. This is done because, when this is being written, the didsdk.Resolution interface does not let callers
+// pass in their own context.
+type LocalResolver struct {
+	*didsdk.Resolver
+}
+
+func (lr LocalResolver) Resolve(_ context.Context, did string, opts ...didsdk.ResolutionOptions) (*didsdk.DIDResolutionResult, error) {
+	return lr.Resolver.Resolve(did, opts...)
+}
diff --git a/pkg/service/did/service.go b/pkg/service/did/service.go
index c17fc6a..33311fe 100644
--- a/pkg/service/did/service.go
+++ b/pkg/service/did/service.go
@@ -3,23 +3,26 @@ package did
 import (
 	"context"
 	"fmt"
+	"net/http"
 
 	didsdk "github.com/TBD54566975/ssi-sdk/did"
 	sdkutil "github.com/TBD54566975/ssi-sdk/util"
 	"github.com/pkg/errors"
-
 	"github.com/tbd54566975/ssi-service/config"
 	"github.com/tbd54566975/ssi-service/internal/did"
 	"github.com/tbd54566975/ssi-service/internal/util"
+	"github.com/tbd54566975/ssi-service/pkg/service/did/resolve"
 	"github.com/tbd54566975/ssi-service/pkg/service/framework"
 	"github.com/tbd54566975/ssi-service/pkg/service/keystore"
 	"github.com/tbd54566975/ssi-service/pkg/storage"
 )
 
 type Service struct {
-	config   config.DIDServiceConfig
-	storage  *Storage
-	resolver *didsdk.Resolver
+	config        config.DIDServiceConfig
+	storage       *Storage
+	localResolver *resolve.LocalResolver
+
+	methodToResolver map[string]resolve.Resolver
 
 	// supported DID methods
 	handlers map[didsdk.Method]MethodHandler
@@ -44,7 +47,7 @@ func (s *Service) Status() framework.Status {
 	if s.keyStore == nil {
 		ae.AppendString("no key store service configured")
 	}
-	if s.resolver == nil {
+	if s.localResolver == nil {
 		ae.AppendString("no resolver configured")
 	}
 	if !ae.IsEmpty() {
@@ -60,8 +63,8 @@ func (s *Service) Config() config.DIDServiceConfig {
 	return s.config
 }
 
-func (s *Service) GetResolver() *didsdk.Resolver {
-	return s.resolver
+func (s *Service) GetResolver() resolve.Resolver {
+	return s
 }
 
 func NewDIDService(config config.DIDServiceConfig, s storage.ServiceStorage, keyStore *keystore.Service) (*Service, error) {
@@ -71,16 +74,29 @@ func NewDIDService(config config.DIDServiceConfig, s storage.ServiceStorage, key
 	}
 
 	// instantiate DID resolver
-	resolver, err := did.BuildResolver(config.ResolutionMethods)
+	sdkResolver, err := did.BuildResolver(config.ResolutionMethods)
 	if err != nil {
 		return nil, errors.Wrap(err, "could not instantiate DID resolver")
 	}
 
 	service := Service{
-		storage:  didStorage,
-		handlers: make(map[didsdk.Method]MethodHandler),
-		keyStore: keyStore,
-		resolver: resolver,
+		storage:          didStorage,
+		handlers:         make(map[didsdk.Method]MethodHandler),
+		keyStore:         keyStore,
+		localResolver:    &resolve.LocalResolver{Resolver: sdkResolver},
+		methodToResolver: make(map[string]resolve.Resolver),
+	}
+
+	for _, sm := range service.localResolver.SupportedMethods() {
+		service.methodToResolver[sm.String()] = service.localResolver
+	}
+
+	ur := &resolve.UniversalResolver{
+		Client: http.Client{},
+		URL:    config.UniversalResolverURL,
+	}
+	for _, urm := range config.UniversalResolverMethods {
+		service.methodToResolver[urm] = ur
 	}
 
 	// instantiate all handlers for DID methods
@@ -119,9 +135,9 @@ func (s *Service) ResolveDID(request ResolveDIDRequest) (*ResolveDIDResponse, er
 	if request.DID == "" {
 		return nil, util.LoggingNewError("cannot resolve empty DID")
 	}
-	resolved, err := s.resolver.Resolve(request.DID)
+	resolved, err := s.Resolve(context.Background(), request.DID)
 	if err != nil {
-		return nil, util.LoggingErrorMsgf(err, "could not resolve DID: %s", request.DID)
+		return nil, err
 	}
 	return &ResolveDIDResponse{
 		ResolutionMetadata:  &resolved.DIDResolutionMetadata,
@@ -130,6 +146,19 @@ func (s *Service) ResolveDID(request ResolveDIDRequest) (*ResolveDIDResponse, er
 	}, nil
 }
 
+func (s *Service) Resolve(ctx context.Context, did string, _ ...didsdk.ResolutionOptions) (*didsdk.DIDResolutionResult, error) {
+	selectedResolver, err := s.chooseResolver(did)
+	if err != nil {
+		return nil, util.LoggingErrorMsg(err, "choosing resolver")
+	}
+
+	resolved, err := selectedResolver.Resolve(ctx, did)
+	if err != nil {
+		return nil, util.LoggingErrorMsgf(err, "could not resolve DID: %s", did)
+	}
+	return resolved, nil
+}
+
 func (s *Service) GetSupportedMethods() GetSupportedMethodsResponse {
 	methods := make([]didsdk.Method, 0, len(s.handlers))
 	for method := range s.handlers {
@@ -170,3 +199,17 @@ func (s *Service) getHandler(method didsdk.Method) (MethodHandler, error) {
 	}
 	return handler, nil
 }
+
+func (s *Service) chooseResolver(did string) (resolve.Resolver, error) {
+	didMethod, err := util.GetMethodForDID(did)
+	if err != nil {
+		return nil, errors.Wrap(err, "getting method for did")
+	}
+
+	r, ok := s.methodToResolver[didMethod]
+	if !ok {
+		return nil, errors.Errorf("resolver for %s not available", didMethod)
+	}
+
+	return r, nil
+}
diff --git a/pkg/service/manifest/application.go b/pkg/service/manifest/application.go
index c89cae6..39ab941 100644
--- a/pkg/service/manifest/application.go
+++ b/pkg/service/manifest/application.go
@@ -15,8 +15,8 @@ import (
 	"github.com/tbd54566975/ssi-service/pkg/service/credential"
 )
 
-func (s Service) verifyApplicationJWT(did string, token keyaccess.JWT) error {
-	return jwt.VerifyTokenFromDID(did, token, s.didResolver)
+func (s Service) verifyApplicationJWT(ctx context.Context, did string, token keyaccess.JWT) error {
+	return jwt.VerifyTokenFromDID(ctx, did, token, s.didResolver)
 }
 
 // validateCredentialApplication validates the credential application's signature(s) in addition to making sure it
@@ -24,7 +24,7 @@ func (s Service) verifyApplicationJWT(did string, token keyaccess.JWT) error {
 // input descriptors along with an error if validation fails.
 func (s Service) validateCredentialApplication(ctx context.Context, credManifest manifest.CredentialManifest, request model.SubmitApplicationRequest) (inputDescriptorIDs []string, err error) {
 	// validate the payload's signature
-	if verificationErr := s.verifyApplicationJWT(request.ApplicantDID, request.ApplicationJWT); verificationErr != nil {
+	if verificationErr := s.verifyApplicationJWT(ctx, request.ApplicantDID, request.ApplicationJWT); verificationErr != nil {
 		err = util.LoggingErrorMsgf(err, "could not verify application<%s>'s signature", request.Application.ID)
 		return
 	}
diff --git a/pkg/service/manifest/manifest.go b/pkg/service/manifest/manifest.go
index 780dc44..1ba36fd 100644
--- a/pkg/service/manifest/manifest.go
+++ b/pkg/service/manifest/manifest.go
@@ -32,7 +32,7 @@ func (s Service) signManifestJWT(ctx context.Context, m CredentialManifestContai
 	return manifestToken, nil
 }
 
-func (s Service) verifyManifestJWT(token keyaccess.JWT) (*manifest.CredentialManifest, error) {
+func (s Service) verifyManifestJWT(ctx context.Context, token keyaccess.JWT) (*manifest.CredentialManifest, error) {
 	parsed, err := jwt.Parse([]byte(token))
 	if err != nil {
 		return nil, util.LoggingErrorMsg(err, "could not parse JWT")
@@ -47,7 +47,7 @@ func (s Service) verifyManifestJWT(token keyaccess.JWT) (*manifest.CredentialMan
 		return nil, util.LoggingErrorMsg(err, "could not unmarshal claims into manifest")
 	}
 	issuer := parsedManifest.Manifest.Issuer.ID
-	kid, pubKey, err := didint.ResolveKeyForDID(s.didResolver, issuer)
+	kid, pubKey, err := didint.ResolveKeyForDID(ctx, s.didResolver, issuer)
 	if err != nil {
 		return nil, util.LoggingErrorMsgf(err, "failed to resolve manifest issuer's did: %s", issuer)
 	}
diff --git a/pkg/service/manifest/service.go b/pkg/service/manifest/service.go
index 321597b..090827f 100644
--- a/pkg/service/manifest/service.go
+++ b/pkg/service/manifest/service.go
@@ -5,7 +5,6 @@ import (
 	"fmt"
 
 	"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/benbjohnson/clock"
@@ -16,6 +15,7 @@ import (
 	credint "github.com/tbd54566975/ssi-service/internal/credential"
 	"github.com/tbd54566975/ssi-service/internal/util"
 	"github.com/tbd54566975/ssi-service/pkg/service/credential"
+	"github.com/tbd54566975/ssi-service/pkg/service/did/resolve"
 	"github.com/tbd54566975/ssi-service/pkg/service/framework"
 	"github.com/tbd54566975/ssi-service/pkg/service/issuing"
 	"github.com/tbd54566975/ssi-service/pkg/service/keystore"
@@ -35,7 +35,7 @@ type Service struct {
 
 	// external dependencies
 	keyStore    *keystore.Service
-	didResolver *didsdk.Resolver
+	didResolver resolve.Resolver
 	credential  *credential.Service
 
 	Clock clock.Clock
@@ -72,13 +72,7 @@ func (s Service) Config() config.ManifestServiceConfig {
 	return s.config
 }
 
-func NewManifestService(
-	config config.ManifestServiceConfig,
-	s storage.ServiceStorage,
-	keyStore *keystore.Service,
-	didResolver *didsdk.Resolver,
-	credential *credential.Service,
-) (*Service, error) {
+func NewManifestService(config config.ManifestServiceConfig, s storage.ServiceStorage, keyStore *keystore.Service, didResolver resolve.Resolver, credential *credential.Service) (*Service, error) {
 	manifestStorage, err := manifeststg.NewManifestStorage(s)
 	if err != nil {
 		return nil, util.LoggingErrorMsg(err, "could not instantiate storage for the manifest service")
@@ -195,8 +189,8 @@ func (s Service) CreateManifest(ctx context.Context, request model.CreateManifes
 }
 
 // VerifyManifest verifies a manifest's signature and makes sure the manifest is compliant with the specification
-func (s Service) VerifyManifest(request model.VerifyManifestRequest) (*model.VerifyManifestResponse, error) {
-	m, err := s.verifyManifestJWT(request.ManifestJWT)
+func (s Service) VerifyManifest(ctx context.Context, request model.VerifyManifestRequest) (*model.VerifyManifestResponse, error) {
+	m, err := s.verifyManifestJWT(ctx, request.ManifestJWT)
 	if err != nil {
 		return &model.VerifyManifestResponse{
 			Verified: false,
diff --git a/pkg/service/presentation/service.go b/pkg/service/presentation/service.go
index b9a22b6..85cf828 100644
--- a/pkg/service/presentation/service.go
+++ b/pkg/service/presentation/service.go
@@ -6,7 +6,6 @@ import (
 
 	"github.com/TBD54566975/ssi-sdk/credential/exchange"
 	"github.com/TBD54566975/ssi-sdk/credential/signing"
-	didsdk "github.com/TBD54566975/ssi-sdk/did"
 	sdkutil "github.com/TBD54566975/ssi-sdk/util"
 	"github.com/pkg/errors"
 	"github.com/sirupsen/logrus"
@@ -14,6 +13,7 @@ import (
 	"github.com/tbd54566975/ssi-service/internal/credential"
 	"github.com/tbd54566975/ssi-service/internal/util"
 	"github.com/tbd54566975/ssi-service/pkg/jwt"
+	"github.com/tbd54566975/ssi-service/pkg/service/did/resolve"
 	"github.com/tbd54566975/ssi-service/pkg/service/framework"
 	"github.com/tbd54566975/ssi-service/pkg/service/keystore"
 	"github.com/tbd54566975/ssi-service/pkg/service/operation"
@@ -30,7 +30,7 @@ type Service struct {
 	keystore   *keystore.Service
 	opsStorage *operation.Storage
 	config     config.PresentationServiceConfig
-	resolver   *didsdk.Resolver
+	resolver   resolve.Resolver
 	schema     *schema.Service
 	verifier   *credential.Verifier
 }
@@ -57,7 +57,7 @@ func (s Service) Config() config.PresentationServiceConfig {
 	return s.config
 }
 
-func NewPresentationService(config config.PresentationServiceConfig, s storage.ServiceStorage, resolver *didsdk.Resolver, schema *schema.Service, keystore *keystore.Service) (*Service, error) {
+func NewPresentationService(config config.PresentationServiceConfig, s storage.ServiceStorage, resolver resolve.Resolver, schema *schema.Service, keystore *keystore.Service) (*Service, error) {
 	presentationStorage, err := NewPresentationStorage(s)
 	if err != nil {
 		return nil, util.LoggingErrorMsg(err, "could not instantiate definition storage for the presentation service")
@@ -164,7 +164,7 @@ func (s Service) CreateSubmission(ctx context.Context, request model.CreateSubmi
 	if err != nil {
 		return nil, errors.Wrap(err, "parsing vp from jwt")
 	}
-	if err := jwt.VerifyTokenFromDID(sdkVP.Holder, request.SubmissionJWT, s.resolver); err != nil {
+	if err := jwt.VerifyTokenFromDID(ctx, sdkVP.Holder, request.SubmissionJWT, s.resolver); err != nil {
 		return nil, errors.Wrap(err, "verifying token from did")
 	}
 
diff --git a/pkg/service/schema/service.go b/pkg/service/schema/service.go
index 474ff6d..4bb14c1 100644
--- a/pkg/service/schema/service.go
+++ b/pkg/service/schema/service.go
@@ -6,7 +6,6 @@ import (
 	"time"
 
 	"github.com/TBD54566975/ssi-sdk/credential/schema"
-	didsdk "github.com/TBD54566975/ssi-sdk/did"
 	schemalib "github.com/TBD54566975/ssi-sdk/schema"
 	sdkutil "github.com/TBD54566975/ssi-sdk/util"
 	"github.com/goccy/go-json"
@@ -14,6 +13,7 @@ import (
 	"github.com/lestrrat-go/jwx/jwt"
 	"github.com/pkg/errors"
 	"github.com/sirupsen/logrus"
+	didresolver "github.com/tbd54566975/ssi-service/pkg/service/did/resolve"
 
 	"github.com/tbd54566975/ssi-service/config"
 	"github.com/tbd54566975/ssi-service/internal/did"
@@ -31,7 +31,7 @@ type Service struct {
 
 	// external dependencies
 	keyStore *keystore.Service
-	resolver *didsdk.Resolver
+	resolver didresolver.Resolver
 }
 
 func (s Service) Type() framework.Type {
@@ -62,7 +62,7 @@ func (s Service) Config() config.SchemaServiceConfig {
 	return s.config
 }
 
-func NewSchemaService(config config.SchemaServiceConfig, s storage.ServiceStorage, keyStore *keystore.Service, resolver *didsdk.Resolver) (*Service, error) {
+func NewSchemaService(config config.SchemaServiceConfig, s storage.ServiceStorage, keyStore *keystore.Service, resolver didresolver.Resolver) (*Service, error) {
 	schemaStorage, err := NewSchemaStorage(s)
 	if err != nil {
 		return nil, util.LoggingErrorMsg(err, "could not instantiate storage for the schema service")
@@ -160,15 +160,15 @@ func (s Service) signSchemaJWT(ctx context.Context, author string, schema schema
 	if err != nil {
 		return nil, util.LoggingErrorMsgf(err, "could not sign schema for author<%s>", author)
 	}
-	if _, err = s.verifySchemaJWT(keyaccess.JWT(schemaToken)); err != nil {
+	if _, err = s.verifySchemaJWT(ctx, keyaccess.JWT(schemaToken)); err != nil {
 		return nil, util.LoggingErrorMsg(err, "could not verify signed schema")
 	}
 	return keyaccess.JWTPtr(string(schemaToken)), nil
 }
 
 // VerifySchema verifies a schema's signature and makes sure the schema is compliant with the specification
-func (s Service) VerifySchema(request VerifySchemaRequest) (*VerifySchemaResponse, error) {
-	credSchema, err := s.verifySchemaJWT(request.SchemaJWT)
+func (s Service) VerifySchema(ctx context.Context, request VerifySchemaRequest) (*VerifySchemaResponse, error) {
+	credSchema, err := s.verifySchemaJWT(ctx, request.SchemaJWT)
 	if err != nil {
 		return &VerifySchemaResponse{Verified: false, Reason: "could not verify schema's signature: " + err.Error()}, nil
 	}
@@ -184,7 +184,7 @@ func (s Service) VerifySchema(request VerifySchemaRequest) (*VerifySchemaRespons
 	return &VerifySchemaResponse{Verified: true}, nil
 }
 
-func (s Service) verifySchemaJWT(token keyaccess.JWT) (*schema.VCJSONSchema, error) {
+func (s Service) verifySchemaJWT(ctx context.Context, token keyaccess.JWT) (*schema.VCJSONSchema, error) {
 	parsed, err := jwt.Parse([]byte(token))
 	if err != nil {
 		return nil, util.LoggingErrorMsg(err, "could not parse JWT")
@@ -198,7 +198,7 @@ func (s Service) verifySchemaJWT(token keyaccess.JWT) (*schema.VCJSONSchema, err
 	if err = json.Unmarshal(claimsJSONBytes, &parsedSchema); err != nil {
 		return nil, util.LoggingErrorMsg(err, "could not unmarshal claims into schema")
 	}
-	resolved, err := s.resolver.Resolve(parsedSchema.Author)
+	resolved, err := s.resolver.Resolve(ctx, parsedSchema.Author)
 	if err != nil {
 		return nil, errors.Wrapf(err, "failed to resolve schema author's did: %s", parsedSchema.Author)
 	}


Test patch:
diff --git a/integration/steelthread_integration_test.go b/integration/steelthread_integration_test.go
index fe4326d..f0d21d1 100644
--- a/integration/steelthread_integration_test.go
+++ b/integration/steelthread_integration_test.go
@@ -26,6 +26,20 @@ func TestCreateIssuerDIDKeyIntegration(t *testing.T) {
 
 }
 
+func TestResolveIONDIDIntegration(t *testing.T) {
+	if testing.Short() {
+		t.Skip("skipping integration test")
+	}
+
+	resolveOutput, err := ResolveDID("did:ion:EiD3DIbDgBCajj2zCkE48x74FKTV9_Dcu1u_imzZddDKfg")
+	assert.NoError(t, err)
+	assert.NotEmpty(t, resolveOutput)
+
+	ionDID, err := getJSONElement(resolveOutput, "$.didDocument.id")
+	assert.NoError(t, err)
+	assert.Equal(t, "did:ion:EiD3DIbDgBCajj2zCkE48x74FKTV9_Dcu1u_imzZddDKfg", ionDID)
+}
+
 func TestCreateAliceDIDKeyIntegration(t *testing.T) {
 	if testing.Short() {
 		t.Skip("skipping integration test")
diff --git a/pkg/server/router/manifest_test.go b/pkg/server/router/manifest_test.go
index adb5f12..a2891a0 100644
--- a/pkg/server/router/manifest_test.go
+++ b/pkg/server/router/manifest_test.go
@@ -96,7 +96,7 @@ func TestManifestRouter(t *testing.T) {
 		assert.NoError(tt, err)
 		assert.NotEmpty(tt, createdManifest)
 
-		verificationResponse, err := manifestService.VerifyManifest(model.VerifyManifestRequest{ManifestJWT: createdManifest.ManifestJWT})
+		verificationResponse, err := manifestService.VerifyManifest(context.Background(), model.VerifyManifestRequest{ManifestJWT: createdManifest.ManifestJWT})
 		assert.NoError(tt, err)
 		assert.NotEmpty(tt, verificationResponse)
 		assert.True(tt, verificationResponse.Verified)
diff --git a/pkg/server/router/schema_test.go b/pkg/server/router/schema_test.go
index 3d586c0..2cbc40e 100644
--- a/pkg/server/router/schema_test.go
+++ b/pkg/server/router/schema_test.go
@@ -170,13 +170,13 @@ func TestSchemaSigning(t *testing.T) {
 		assert.NotEmpty(tt, createdSchema.SchemaJWT)
 
 		// verify the schema
-		verifiedSchema, err := schemaService.VerifySchema(schema.VerifySchemaRequest{SchemaJWT: *createdSchema.SchemaJWT})
+		verifiedSchema, err := schemaService.VerifySchema(context.Background(), schema.VerifySchemaRequest{SchemaJWT: *createdSchema.SchemaJWT})
 		assert.NoError(tt, err)
 		assert.NotEmpty(tt, verifiedSchema)
 		assert.True(tt, verifiedSchema.Verified)
 
 		// verify a bad schema
-		verifiedSchema, err = schemaService.VerifySchema(schema.VerifySchemaRequest{SchemaJWT: "bad"})
+		verifiedSchema, err = schemaService.VerifySchema(context.Background(), schema.VerifySchemaRequest{SchemaJWT: "bad"})
 		assert.NoError(tt, err)
 		assert.NotEmpty(tt, verifiedSchema)
 		assert.False(tt, verifiedSchema.Verified)
diff --git a/pkg/server/server_did_test.go b/pkg/server/server_did_test.go
index 06ed88b..72d4244 100644
--- a/pkg/server/server_did_test.go
+++ b/pkg/server/server_did_test.go
@@ -254,7 +254,7 @@ func TestDIDAPI(t *testing.T) {
 		}
 		err := didService.ResolveDID(newRequestContextWithParams(badParams), w, req)
 		assert.Error(tt, err)
-		assert.Contains(tt, err.Error(), "not a valid did: bad")
+		assert.Contains(tt, err.Error(), "malformed did: bad")
 
 		w.Flush()
 
diff --git a/pkg/server/server_manifest_test.go b/pkg/server/server_manifest_test.go
index 9c72f38..d252481 100644
--- a/pkg/server/server_manifest_test.go
+++ b/pkg/server/server_manifest_test.go
@@ -100,7 +100,7 @@ func TestManifestAPI(t *testing.T) {
 		assert.Equal(tt, resp.Manifest.Issuer.ID, issuerDID.DID.ID)
 
 		// verify the manifest
-		verificationResponse, err := manifestService.VerifyManifest(manifestsvc.VerifyManifestRequest{ManifestJWT: resp.ManifestJWT})
+		verificationResponse, err := manifestService.VerifyManifest(context.Background(), manifestsvc.VerifyManifestRequest{ManifestJWT: resp.ManifestJWT})
 		assert.NoError(tt, err)
 		assert.NotEmpty(tt, verificationResponse)
 		assert.True(tt, verificationResponse.Verified)
@@ -242,7 +242,7 @@ func TestManifestAPI(t *testing.T) {
 
 		// verify each manifest
 		for _, m := range getManifestsResp.Manifests {
-			verificationResponse, err := manifestService.VerifyManifest(manifestsvc.VerifyManifestRequest{ManifestJWT: m.ManifestJWT})
+			verificationResponse, err := manifestService.VerifyManifest(context.Background(), manifestsvc.VerifyManifestRequest{ManifestJWT: m.ManifestJWT})
 			assert.NoError(tt, err)
 			assert.NotEmpty(tt, verificationResponse)
 			assert.True(tt, verificationResponse.Verified)