Repository: TBD54566975/ssi-service
Pull request #562 (2023-06-23 22:12:18)

Support adding/removing services and keys to an ion DID
# Overview
Support adding/removing services and keys to an ion DID. Must be custodied.

# Description
This is the last part on #340. Most of the complexity comes from needing to ensure concurrent updates work.

Note that this PR uses the same abstractions from the SDK.

# How Has This Been Tested?
Wrote a unit test.



codecov-commenter: ## [Codecov](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/562?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) Report
> Merging [#562](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/562?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) (e70388b) into [main](https://app.codecov.io/gh/TBD54566975/ssi-service/commit/0e4d0fc6cba2cf64958355e8b80cc7916f1a0140?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) (0e4d0fc) will **decrease** coverage by `1.31%`.
> The diff coverage is `4.10%`.

```diff
@@            Coverage Diff             @@
##             main     #562      +/-   ##
==========================================
- Coverage   23.98%   22.67%   -1.31%     
==========================================
  Files          46       46              
  Lines        5119     5472     +353     
==========================================
+ Hits         1228     1241      +13     
- Misses       3663     4002     +339     
- Partials      228      229       +1     
```


| [Impacted Files](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/562?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.go](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/562?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-cGtnL3NlcnZlci9yb3V0ZXIvZGlkLmdv) | `3.37% <0.00%> (-1.00%)` | :arrow_down: |
| [pkg/service/did/service.go](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/562?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-cGtnL3NlcnZpY2UvZGlkL3NlcnZpY2UuZ28=) | `0.00% <0.00%> (ø)` | |
| [pkg/service/did/ion.go](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/562?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-cGtnL3NlcnZpY2UvZGlkL2lvbi5nbw==) | `27.66% <3.37%> (-34.22%)` | :arrow_down: |
| [pkg/server/server.go](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/562?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-cGtnL3NlcnZlci9zZXJ2ZXIuZ28=) | `58.96% <100.00%> (+0.16%)` | :arrow_up: |
| [pkg/service/did/storage.go](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/562?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-cGtnL3NlcnZpY2UvZGlkL3N0b3JhZ2UuZ28=) | `56.08% <100.00%> (+0.60%)` | :arrow_up: |


nitro-neal: if we remove pub keys / add other pub keys, and the private keys are not in the system, this will be like a "gimp" did.

It wont be able to do things like issue a vc and stuff, I think we have good error messages that would explain that if you try to do something like that.


Patch:
diff --git a/doc/swagger.yaml b/doc/swagger.yaml
index f750e43..93b92bb 100644
--- a/doc/swagger.yaml
+++ b/doc/swagger.yaml
@@ -875,6 +875,33 @@ definitions:
         description: Whether the DIDConfiguration was verified.
         type: boolean
     type: object
+  ion.PublicKey:
+    properties:
+      id:
+        type: string
+      publicKeyJwk:
+        $ref: '#/definitions/jwx.PublicKeyJWK'
+      purposes:
+        items:
+          $ref: '#/definitions/ion.PublicKeyPurpose'
+        type: array
+      type:
+        type: string
+    type: object
+  ion.PublicKeyPurpose:
+    enum:
+    - authentication
+    - assertionMethod
+    - capabilityInvocation
+    - capabilityDelegation
+    - keyAgreement
+    type: string
+    x-enum-varnames:
+    - Authentication
+    - AssertionMethod
+    - CapabilityInvocation
+    - CapabilityDelegation
+    - KeyAgreement
   jwx.PublicKeyJWK:
     properties:
       alg:
@@ -1904,6 +1931,25 @@ definitions:
       id:
         type: string
     type: object
+  pkg_server_router.StateChange:
+    properties:
+      publicKeyIdsToRemove:
+        items:
+          type: string
+        type: array
+      publicKeysToAdd:
+        items:
+          $ref: '#/definitions/ion.PublicKey'
+        type: array
+      serviceIdsToRemove:
+        items:
+          type: string
+        type: array
+      servicesToAdd:
+        items:
+          $ref: '#/definitions/github_com_TBD54566975_ssi-sdk_did.Service'
+        type: array
+    type: object
   pkg_server_router.StoreKeyRequest:
     properties:
       base58PrivateKey:
@@ -1971,6 +2017,21 @@ definitions:
       suspended:
         type: boolean
     type: object
+  pkg_server_router.UpdateDIDByMethodRequest:
+    properties:
+      stateChange:
+        allOf:
+        - $ref: '#/definitions/pkg_server_router.StateChange'
+        description: Expected to be populated when `method == "ion"`. Describes the
+          changes that are requested.
+    required:
+    - stateChange
+    type: object
+  pkg_server_router.UpdateDIDByMethodResponse:
+    properties:
+      did:
+        $ref: '#/definitions/did.Document'
+    type: object
   pkg_server_router.VerifyCredentialRequest:
     properties:
       credential:
@@ -2803,6 +2864,47 @@ paths:
       summary: Get a DID
       tags:
       - DecentralizedIdentifiers
+    put:
+      consumes:
+      - application/json
+      description: |-
+        Updates a DID for which SSI is the custodian. The DID must have been previously created by calling
+        the "Create DID Document" endpoint. Currently, only ION dids support updates.
+      parameters:
+      - description: Method
+        in: path
+        name: method
+        required: true
+        type: string
+      - description: ID
+        in: path
+        name: id
+        required: true
+        type: string
+      - description: request body
+        in: body
+        name: request
+        required: true
+        schema:
+          $ref: '#/definitions/pkg_server_router.UpdateDIDByMethodRequest'
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/pkg_server_router.UpdateDIDByMethodResponse'
+        "400":
+          description: Bad request
+          schema:
+            type: string
+        "500":
+          description: Internal server error
+          schema:
+            type: string
+      summary: Updates a DID document.
+      tags:
+      - DecentralizedIdentityAPI
   /v1/dids/{method}/batch:
     put:
       consumes:
diff --git a/integration/common.go b/integration/common.go
index 09628ce..3070a9d 100644
--- a/integration/common.go
+++ b/integration/common.go
@@ -123,6 +123,15 @@ func CreateDIDION() (string, error) {
 	return output, nil
 }
 
+func UpdateDIDION(id string) (string, error) {
+	output, err := put(endpoint+version+"dids/ion/"+id, getJSONFromFile("update-did-ion-input.json"))
+	if err != nil {
+		return "", errors.Wrapf(err, "did endpoint with output: %s", output)
+	}
+
+	return output, nil
+}
+
 func ListWebDIDs() (string, error) {
 	urlValues := url.Values{
 		"pageSize": []string{"10"},
diff --git a/pkg/server/router/did.go b/pkg/server/router/did.go
index 28cddee..208ee47 100644
--- a/pkg/server/router/did.go
+++ b/pkg/server/router/did.go
@@ -7,6 +7,7 @@ import (
 
 	"github.com/TBD54566975/ssi-sdk/crypto"
 	didsdk "github.com/TBD54566975/ssi-sdk/did"
+	"github.com/TBD54566975/ssi-sdk/did/ion"
 	"github.com/TBD54566975/ssi-sdk/did/resolution"
 	"github.com/gin-gonic/gin"
 	"github.com/goccy/go-json"
@@ -127,6 +128,100 @@ func (dr DIDRouter) CreateDIDByMethod(c *gin.Context) {
 	framework.Respond(c, resp, http.StatusCreated)
 }
 
+type StateChange struct {
+	ServicesToAdd        []didsdk.Service `json:"servicesToAdd,omitempty"`
+	ServiceIDsToRemove   []string         `json:"serviceIdsToRemove,omitempty"`
+	PublicKeysToAdd      []ion.PublicKey  `json:"publicKeysToAdd,omitempty"`
+	PublicKeyIDsToRemove []string         `json:"publicKeyIdsToRemove"`
+}
+
+type UpdateDIDByMethodRequest struct {
+	// Expected to be populated when `method == "ion"`. Describes the changes that are requested.
+	StateChange StateChange `json:"stateChange" validate:"required"`
+}
+
+type UpdateDIDByMethodResponse struct {
+	DID didsdk.Document `json:"did,omitempty"`
+}
+
+// UpdateDIDByMethod godoc
+//
+//	@Summary		Updates a DID document.
+//	@Description	Updates a DID for which SSI is the custodian. The DID must have been previously created by calling
+//	@Description	the "Create DID Document" endpoint. Currently, only ION dids support updates.
+//	@Tags			DecentralizedIdentityAPI
+//	@Accept			json
+//	@Produce		json
+//	@Param			method	path		string						true	"Method"
+//	@Param			id		path		string						true	"ID"
+//	@Param			request	body		UpdateDIDByMethodRequest	true	"request body"
+//	@Success		200		{object}	UpdateDIDByMethodResponse
+//	@Failure		400		{string}	string	"Bad request"
+//	@Failure		500		{string}	string	"Internal server error"
+//	@Router			/v1/dids/{method}/{id} [put]
+func (dr DIDRouter) UpdateDIDByMethod(c *gin.Context) {
+	method := framework.GetParam(c, MethodParam)
+	if method == nil {
+		errMsg := "update DID by method request missing method parameter"
+		framework.LoggingRespondErrMsg(c, errMsg, http.StatusBadRequest)
+		return
+	}
+	if *method != didsdk.IONMethod.String() {
+		framework.LoggingRespondErrMsg(c, "ion is the only method supported", http.StatusBadRequest)
+	}
+
+	id := framework.GetParam(c, IDParam)
+	if id == nil {
+		errMsg := fmt.Sprintf("update DID request missing id parameter for method: %s", *method)
+		framework.LoggingRespondErrMsg(c, errMsg, http.StatusBadRequest)
+		return
+	}
+	var request UpdateDIDByMethodRequest
+	invalidRequest := "invalid update DID 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
+	}
+
+	updateDIDRequest, err := toUpdateIONDIDRequest(*id, request)
+	if err != nil {
+		errMsg := fmt.Sprintf("%s: could not update DID for method<%s>", invalidRequest, *method)
+		framework.LoggingRespondErrWithMsg(c, err, errMsg, http.StatusBadRequest)
+		return
+	}
+	updateIONDIDResponse, err := dr.service.UpdateIONDID(c, *updateDIDRequest)
+	if err != nil {
+		errMsg := fmt.Sprintf("could not update DID for method<%s>", *method)
+		framework.LoggingRespondErrWithMsg(c, err, errMsg, http.StatusInternalServerError)
+		return
+	}
+
+	resp := CreateDIDByMethodResponse{DID: updateIONDIDResponse.DID}
+	framework.Respond(c, resp, http.StatusOK)
+
+}
+
+func toUpdateIONDIDRequest(id string, request UpdateDIDByMethodRequest) (*did.UpdateIONDIDRequest, error) {
+	didION := ion.ION(id)
+	if !didION.IsValid() {
+		return nil, errors.Errorf("invalid ion did %s", id)
+	}
+	return &did.UpdateIONDIDRequest{
+		DID: didION,
+		StateChange: ion.StateChange{
+			ServicesToAdd:        request.StateChange.ServicesToAdd,
+			ServiceIDsToRemove:   request.StateChange.ServiceIDsToRemove,
+			PublicKeysToAdd:      request.StateChange.PublicKeysToAdd,
+			PublicKeyIDsToRemove: request.StateChange.PublicKeyIDsToRemove,
+		},
+	}, nil
+}
+
 // toCreateDIDRequest converts CreateDIDByMethodRequest to did.CreateDIDRequest, parsing options according to method
 func toCreateDIDRequest(m didsdk.Method, request CreateDIDByMethodRequest) (*did.CreateDIDRequest, error) {
 	createRequest := did.CreateDIDRequest{
diff --git a/pkg/server/server.go b/pkg/server/server.go
index dbc7604..6e3bb28 100644
--- a/pkg/server/server.go
+++ b/pkg/server/server.go
@@ -170,6 +170,7 @@ func DecentralizedIdentityAPI(rg *gin.RouterGroup, service *didsvc.Service, did
 	didAPI := rg.Group(DIDsPrefix)
 	didAPI.GET("", didRouter.ListDIDMethods)
 	didAPI.PUT("/:method", middleware.Webhook(webhookService, webhook.DID, webhook.Create), didRouter.CreateDIDByMethod)
+	didAPI.PUT("/:method/:id", didRouter.UpdateDIDByMethod)
 	didAPI.PUT("/:method/batch", middleware.Webhook(webhookService, webhook.DID, webhook.BatchCreate), batchDIDRouter.BatchCreateDIDs)
 	didAPI.GET("/:method", didRouter.ListDIDsByMethod)
 	didAPI.GET("/:method/:id", didRouter.GetDIDByMethod)
diff --git a/pkg/service/did/ion.go b/pkg/service/did/ion.go
index f817def..ceb83d2 100644
--- a/pkg/service/did/ion.go
+++ b/pkg/service/did/ion.go
@@ -9,7 +9,6 @@ import (
 	"github.com/TBD54566975/ssi-sdk/crypto/jwx"
 	"github.com/TBD54566975/ssi-sdk/did"
 	"github.com/TBD54566975/ssi-sdk/did/ion"
-	"github.com/TBD54566975/ssi-sdk/did/resolution"
 	"github.com/TBD54566975/ssi-sdk/util"
 	"github.com/goccy/go-json"
 	"github.com/google/uuid"
@@ -20,6 +19,7 @@ import (
 
 	"github.com/tbd54566975/ssi-service/pkg/service/common"
 	"github.com/tbd54566975/ssi-service/pkg/service/keystore"
+	"github.com/tbd54566975/ssi-service/pkg/storage"
 )
 
 const (
@@ -27,7 +27,7 @@ const (
 	recoverKeySuffix string = "recover"
 )
 
-func NewIONHandler(baseURL string, s *Storage, ks *keystore.Service) (MethodHandler, error) {
+func NewIONHandler(baseURL string, s *Storage, ks *keystore.Service, factory keystore.ServiceFactory, storageFactory StorageFactory) (MethodHandler, error) {
 	if baseURL == "" {
 		return nil, errors.New("baseURL cannot be empty")
 	}
@@ -41,14 +41,23 @@ func NewIONHandler(baseURL string, s *Storage, ks *keystore.Service) (MethodHand
 	if err != nil {
 		return nil, errors.Wrap(err, "creating ion resolver")
 	}
-	return &ionHandler{method: did.IONMethod, resolver: r, storage: s, keyStore: ks}, nil
+	return &ionHandler{
+		method:            did.IONMethod,
+		resolver:          r,
+		storage:           s,
+		keyStore:          ks,
+		keyStoreFactory:   factory,
+		didStorageFactory: storageFactory,
+	}, nil
 }
 
 type ionHandler struct {
-	method   did.Method
-	resolver *ion.Resolver
-	storage  *Storage
-	keyStore *keystore.Service
+	method            did.Method
+	resolver          *ion.Resolver
+	storage           *Storage
+	keyStore          *keystore.Service
+	keyStoreFactory   keystore.ServiceFactory
+	didStorageFactory StorageFactory
 }
 
 // Verify interface compliance https://github.com/uber-go/guide/blob/master/style.md#verify-interface-compliance
@@ -95,6 +104,250 @@ func (i ionStoredDID) IsSoftDeleted() bool {
 	return i.SoftDeleted
 }
 
+type PreAnchor struct {
+	UpdateOperation        *ion.UpdateRequest
+	NextUpdatePublicJWK    *jwx.PublicKeyJWK
+	UpdatedDID             *ionStoredDID
+	NextUpdatePrivateJWKID string
+}
+
+type Anchor struct {
+	// The result of calling anchor.
+	Err string
+}
+
+type updateState struct {
+	ID        string
+	Status    UpdateRequestStatus
+	PreAnchor *PreAnchor
+	Anchor    *Anchor
+}
+
+func (h *ionHandler) UpdateDID(ctx context.Context, request UpdateIONDIDRequest) (*UpdateIONDIDResponse, error) {
+	if err := request.StateChange.IsValid(); err != nil {
+		return nil, errors.Wrap(err, "validating StateChange")
+	}
+
+	updateStatesKey := request.DID.String()
+	watchKeys := []storage.WatchKey{
+		{
+			Namespace: updateRequestStatesNamespace,
+			Key:       updateStatesKey,
+		},
+	}
+
+	execResp, err := h.storage.db.Execute(ctx, h.prepareUpdate(request), watchKeys)
+	if err != nil {
+		return nil, errors.Wrapf(err, "executing transition to %s", PreAnchorStatus)
+	}
+	updateStates := execResp.([]updateState)
+	state := &updateStates[len(updateStates)-1]
+
+	if state.Status == PreAnchorStatus {
+		state.Anchor = new(Anchor)
+		_, err := h.resolver.Anchor(ctx, state.PreAnchor.UpdateOperation)
+		if err != nil {
+			// Signature errors are OK, as they mean that the update operation has already been applied. It means we haven't updated our updateKey to the latest one.
+			state.Anchor.Err = err.Error()
+			if isPreviouslyAnchoredError(err) {
+				state.Status = AnchoredStatus
+			} else {
+				state.Status = AnchorErrorStatus
+				if storeErr := h.storeUpdateStates(ctx, h.storage.db, request.DID.String(), updateStates); storeErr != nil {
+					return nil, storeErr
+				}
+				return nil, err
+			}
+		} else {
+			state.Status = AnchoredStatus
+		}
+		if err := h.storeUpdateStates(ctx, h.storage.db, request.DID.String(), updateStates); err != nil {
+			return nil, err
+		}
+	}
+
+	_, err = h.storage.db.Execute(ctx, h.applyUpdate(state.ID), watchKeys)
+	if err != nil {
+		return nil, errors.Wrapf(err, "executing transition to %s", DoneStatus)
+	}
+
+	return &UpdateIONDIDResponse{
+		DID: state.PreAnchor.UpdatedDID.DID,
+	}, nil
+}
+
+func (h *ionHandler) applyUpdate(id string) func(ctx context.Context, tx storage.Tx) (any, error) {
+	return func(ctx context.Context, tx storage.Tx) (any, error) {
+		updateStates, _, err := h.readUpdateStates(ctx, id)
+		if err != nil {
+			return nil, err
+		}
+		state := &updateStates[len(updateStates)-1]
+		if state.Status == AnchoredStatus {
+			keyStore, err := h.keyStoreFactory(tx)
+			if err != nil {
+				return nil, errors.Wrap(err, "creating key store service")
+			}
+
+			gotKey, err := keyStore.GetKey(ctx, keystore.GetKeyRequest{ID: state.PreAnchor.NextUpdatePrivateJWKID})
+			if err != nil {
+				return nil, errors.Wrap(err, "getting key from keystore")
+			}
+			_, nextUpdatePrivateJWK, err := jwx.PrivateKeyToPrivateKeyJWK(gotKey.ID, gotKey.Key)
+			if err != nil {
+				return nil, errors.Wrap(err, "converting stored key to JWK")
+			}
+
+			updateStoreRequest, err := keyToStoreRequest(updateKeyID(state.ID), *nextUpdatePrivateJWK, state.ID)
+			if err != nil {
+				return nil, errors.Wrap(err, "converting update private key to store request")
+			}
+			if err := keyStore.StoreKey(ctx, *updateStoreRequest); err != nil {
+				return nil, errors.Wrap(err, "could not store did:ion update private key")
+			}
+
+			didStorage, err := h.didStorageFactory(tx)
+			if err != nil {
+				return nil, errors.Wrap(err, "creating did storage")
+			}
+			if err := didStorage.StoreDID(ctx, state.PreAnchor.UpdatedDID); err != nil {
+				return nil, errors.Wrap(err, "storing DID in storage")
+			}
+
+			state.Status = DoneStatus
+			if err := h.storeUpdateStates(ctx, tx, state.ID, updateStates); err != nil {
+				return nil, err
+			}
+		}
+		return nil, nil
+	}
+}
+
+func (h *ionHandler) prepareUpdate(request UpdateIONDIDRequest) func(ctx context.Context, tx storage.Tx) (any, error) {
+	return func(ctx context.Context, tx storage.Tx) (any, error) {
+		updateStates, updatePrivateKey, err := h.readUpdateStates(ctx, request.DID.String())
+		if err != nil {
+			return nil, err
+		}
+		state := &updateStates[len(updateStates)-1]
+		if state.Status == DoneStatus || state.Status == AnchorErrorStatus {
+			updateStates = append(updateStates, updateState{
+				ID: request.DID.String(),
+			})
+			state = &updateStates[len(updateStates)-1]
+		}
+		if state.Status == "" {
+
+			didSuffix, err := request.DID.Suffix()
+			if err != nil {
+				return nil, errors.Wrap(err, "getting did suffix")
+			}
+
+			updateKey := updatePrivateKey.ToPublicKeyJWK()
+			// ION does not like keys that have KID nor ALG. See https://github.com/decentralized-identity/sidetree-reference-impl/blob/bf1f7aeab251083cfb5ea5d612f481cd41f0ab1b/lib/core/versions/latest/util/Jwk.ts#L35
+			updateKey.ALG = ""
+			updateKey.KID = ""
+
+			signer, err := ion.NewBTCSignerVerifier(*updatePrivateKey)
+			if err != nil {
+				return nil, errors.Wrap(err, "creating btc signer verifier")
+			}
+
+			nextUpdateKey, nextUpdatePrivateKey, err := h.nextUpdateKey()
+			if err != nil {
+				return nil, err
+			}
+
+			updateOp, err := ion.NewUpdateRequest(didSuffix, updateKey, *nextUpdateKey, *signer, request.StateChange)
+			if err != nil {
+				return nil, errors.Wrap(err, "creating update request")
+			}
+
+			keyStore, err := h.keyStoreFactory(tx)
+			if err != nil {
+				return nil, errors.Wrap(err, "creating key store service")
+			}
+			storeRequestForUpdateKey, err := keyToStoreRequest("staging:"+request.DID.String(), *nextUpdatePrivateKey, request.DID.String())
+			if err != nil {
+				return nil, errors.Wrap(err, "converting update private key to store request")
+			}
+			if err := keyStore.StoreKey(ctx, *storeRequestForUpdateKey); err != nil {
+				return nil, errors.Wrap(err, "could not store did:ion update private key")
+			}
+
+			storedDID := new(ionStoredDID)
+			if err := h.storage.GetDID(ctx, request.DID.String(), storedDID); err != nil {
+				return nil, errors.Wrap(err, "getting ion did from storage")
+			}
+
+			updatedLongForm, updatedDIDDoc, err := updateLongForm(request.DID.String(), storedDID.LongFormDID, updateOp)
+			if err != nil {
+				return nil, err
+			}
+
+			updatedDID := &ionStoredDID{
+				ID:          storedDID.ID,
+				DID:         *updatedDIDDoc,
+				SoftDeleted: storedDID.SoftDeleted,
+				LongFormDID: updatedLongForm,
+				Operations:  append(storedDID.Operations, updateOp),
+			}
+
+			state.PreAnchor = &PreAnchor{
+				UpdateOperation:        updateOp,
+				UpdatedDID:             updatedDID,
+				NextUpdatePrivateJWKID: storeRequestForUpdateKey.ID,
+				NextUpdatePublicJWK:    nextUpdateKey,
+			}
+			state.Status = PreAnchorStatus
+			if err := h.storeUpdateStates(ctx, tx, request.DID.String(), updateStates); err != nil {
+				return nil, err
+			}
+		}
+
+		return updateStates, nil
+	}
+}
+
+func updateLongForm(shortFormDID string, longFormDID string, updateOp *ion.UpdateRequest) (string, *did.Document, error) {
+	_, initialState, err := ion.DecodeLongFormDID(longFormDID)
+	if err != nil {
+		return "", nil, errors.Wrap(err, "invalid long form DID")
+	}
+
+	delta := ion.Delta{
+		Patches:          append(initialState.Delta.Patches, updateOp.Delta.GetPatches()...),
+		UpdateCommitment: updateOp.Delta.UpdateCommitment,
+	}
+	suffixData := initialState.SuffixData
+	createRequest := ion.CreateRequest{
+		Type:       ion.Create,
+		SuffixData: suffixData,
+		Delta:      delta,
+	}
+	updatedInitialState := ion.InitialState{
+		Delta:      createRequest.Delta,
+		SuffixData: createRequest.SuffixData,
+	}
+	initialStateBytesCanonical, err := ion.CanonicalizeAny(updatedInitialState)
+	if err != nil {
+		return "", nil, errors.Wrap(err, "canonicalizing long form DID suffix data")
+	}
+	encoded := ion.Encode(initialStateBytesCanonical)
+	newLongFormDID := shortFormDID + ":" + encoded
+
+	didDoc, err := ion.PatchesToDIDDocument(shortFormDID, newLongFormDID, createRequest.Delta.GetPatches())
+	if err != nil {
+		return "", nil, errors.Wrap(err, "patching the updated did")
+	}
+	return newLongFormDID, didDoc, nil
+}
+
+func isPreviouslyAnchoredError(_ error) bool {
+	// TODO: figure out how to determine this error from the body of the response.
+	return false
+}
+
 func (h *ionHandler) CreateDID(ctx context.Context, request CreateDIDRequest) (*CreateDIDResponse, error) {
 	// process options
 	var opts CreateIONDIDOptions
@@ -171,15 +424,24 @@ func (h *ionHandler) CreateDID(ctx context.Context, request CreateDIDRequest) (*
 	}
 
 	// submit the create operation to the ION service
-	var resolutionResult *resolution.Result
-	if resolutionResult, err = h.resolver.Anchor(ctx, createOp); err != nil {
+	if _, err = h.resolver.Anchor(ctx, createOp); err != nil {
 		return nil, errors.Wrap(err, "anchoring create operation")
 	}
 
+	_, initialState, err := ion.DecodeLongFormDID(ionDID.LongForm())
+	if err != nil {
+		return nil, errors.Wrap(err, "invalid long form DID")
+	}
+	// TODO: remove the first parameter once it is removed in the SDK (https://github.com/TBD54566975/ssi-sdk/issues/438)
+	didDoc, err := ion.PatchesToDIDDocument("unused", ionDID.ID(), initialState.Delta.Patches)
+	if err != nil {
+		return nil, errors.Wrap(err, "patching the did document locally")
+	}
+
 	// store the did document
 	storedDID := ionStoredDID{
-		ID:          resolutionResult.Document.ID,
-		DID:         resolutionResult.Document,
+		ID:          ionDID.ID(),
+		DID:         *didDoc,
 		SoftDeleted: false,
 		LongFormDID: ionDID.LongForm(),
 		Operations:  ionDID.Operations(),
@@ -188,36 +450,43 @@ func (h *ionHandler) CreateDID(ctx context.Context, request CreateDIDRequest) (*
 		return nil, errors.Wrap(err, "storing ion did document")
 	}
 
-	// store associated keys
-	// 1. update key
-	// 2. recovery key
+	if err := h.storeKeys(ctx, ionDID); err != nil {
+		return nil, err
+	}
+
 	// 3. key(s) in the did docs
-	updateStoreRequest, err := keyToStoreRequest(resolutionResult.Document.ID+"#"+updateKeySuffix, ionDID.GetUpdatePrivateKey(), resolutionResult.Document.ID)
+	keyStoreRequest, err := keyToStoreRequest(did.FullyQualifiedVerificationMethodID(ionDID.ID(), didDoc.VerificationMethod[0].ID), *privKeyJWK, ionDID.ID())
 	if err != nil {
-		return nil, errors.Wrap(err, "converting update private key to store request")
+		return nil, errors.Wrap(err, "converting private key to store request")
 	}
-	if err = h.keyStore.StoreKey(ctx, *updateStoreRequest); err != nil {
-		return nil, errors.Wrap(err, "could not store did:ion update private key")
+	if err = h.keyStore.StoreKey(ctx, *keyStoreRequest); err != nil {
+		return nil, errors.Wrap(err, "could not store did:ion private key")
 	}
 
-	recoveryStoreRequest, err := keyToStoreRequest(resolutionResult.Document.ID+"#"+recoverKeySuffix, ionDID.GetRecoveryPrivateKey(), resolutionResult.Document.ID)
+	return &CreateDIDResponse{DID: *didDoc}, nil
+}
+
+func (h *ionHandler) storeKeys(ctx context.Context, ionDID *ion.DID) error {
+	// store associated keys
+	// 1. update key
+	// 2. recovery key
+	updateStoreRequest, err := keyToStoreRequest(updateKeyID(ionDID.ID()), ionDID.GetUpdatePrivateKey(), ionDID.ID())
 	if err != nil {
-		return nil, errors.Wrap(err, "converting recovery private key to store request")
+		return errors.Wrap(err, "converting update private key to store request")
 	}
-	if err = h.keyStore.StoreKey(ctx, *recoveryStoreRequest); err != nil {
-		return nil, errors.Wrap(err, "could not store did:ion recovery private key")
+	if err = h.keyStore.StoreKey(ctx, *updateStoreRequest); err != nil {
+		return errors.Wrap(err, "could not store did:ion update private key")
 	}
 
-	keyStoreID := did.FullyQualifiedVerificationMethodID(resolutionResult.Document.ID, resolutionResult.Document.VerificationMethod[0].ID)
-	keyStoreRequest, err := keyToStoreRequest(keyStoreID, *privKeyJWK, resolutionResult.Document.ID)
+	recoveryStoreRequest, err := keyToStoreRequest(recoveryKeyID(ionDID.ID()), ionDID.GetRecoveryPrivateKey(), ionDID.ID())
 	if err != nil {
-		return nil, errors.Wrap(err, "converting private key to store request")
+		return errors.Wrap(err, "converting recovery private key to store request")
 	}
-	if err = h.keyStore.StoreKey(ctx, *keyStoreRequest); err != nil {
-		return nil, errors.Wrap(err, "could not store did:ion private key")
+	if err = h.keyStore.StoreKey(ctx, *recoveryStoreRequest); err != nil {
+		return errors.Wrap(err, "could not store did:ion recovery private key")
 	}
 
-	return &CreateDIDResponse{DID: storedDID.DID}, nil
+	return nil
 }
 
 func keyToStoreRequest(kid string, privateKeyJWK jwx.PrivateKeyJWK, controller string) (*keystore.StoreKeyRequest, error) {
@@ -317,3 +586,75 @@ func (h *ionHandler) SoftDeleteDID(ctx context.Context, request DeleteDIDRequest
 
 	return h.storage.StoreDID(ctx, *gotDID)
 }
+
+func (h *ionHandler) readUpdatePrivateKey(ctx context.Context, did string) (*jwx.PrivateKeyJWK, error) {
+	keyID := updateKeyID(did)
+	getKeyRequest := keystore.GetKeyRequest{ID: keyID}
+	key, err := h.keyStore.GetKey(ctx, getKeyRequest)
+	if err != nil {
+		return nil, errors.Wrap(err, "fetching update private key")
+	}
+	_, privateJWK, err := jwx.PrivateKeyToPrivateKeyJWK(keyID, key.Key)
+	if err != nil {
+		return nil, errors.Wrap(err, "getting update private key")
+	}
+	return privateJWK, err
+}
+
+func updateKeyID(did string) string {
+	return did + "#" + updateKeySuffix
+}
+
+func recoveryKeyID(did string) string {
+	return did + "#" + recoverKeySuffix
+}
+
+func (h *ionHandler) nextUpdateKey() (*jwx.PublicKeyJWK, *jwx.PrivateKeyJWK, error) {
+	_, nextUpdatePrivateKey, err := crypto.GenerateSECP256k1Key()
+	if err != nil {
+		return nil, nil, errors.Wrap(err, "generating next update keypair")
+	}
+	nextUpdatePubKeyJWK, nextUpdatePrivateKeyJWK, err := jwx.PrivateKeyToPrivateKeyJWK(uuid.NewString(), nextUpdatePrivateKey)
+	if err != nil {
+		return nil, nil, errors.Wrap(err, "converting next update key pair to JWK")
+	}
+	return nextUpdatePubKeyJWK, nextUpdatePrivateKeyJWK, nil
+}
+
+const updateRequestStatesNamespace = "update-request-states"
+
+func (h *ionHandler) readUpdateStates(ctx context.Context, id string) ([]updateState, *jwx.PrivateKeyJWK, error) {
+	privateUpdateJWK, err := h.readUpdatePrivateKey(ctx, id)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	readData, err := h.storage.db.Read(ctx, updateRequestStatesNamespace, id)
+	if err != nil {
+		return nil, nil, errors.Wrap(err, "reading update status")
+	}
+	if readData == nil {
+		return []updateState{{
+			ID: id,
+		}}, privateUpdateJWK, nil
+	}
+	var statuses []updateState
+	if err := json.Unmarshal(readData, &statuses); err != nil {
+		return nil, nil, errors.Wrap(err, "unmarhsalling status array")
+	}
+
+	return statuses, privateUpdateJWK, nil
+
+}
+
+func (h *ionHandler) storeUpdateStates(ctx context.Context, tx storage.Tx, id string, states []updateState) error {
+	bytes, err := json.Marshal(states)
+	if err != nil {
+		return errors.Wrap(err, "marshalling json")
+	}
+
+	if err := tx.Write(ctx, updateRequestStatesNamespace, id, bytes); err != nil {
+		return errors.Wrap(err, "writing update states")
+	}
+	return nil
+}
diff --git a/pkg/service/did/model.go b/pkg/service/did/model.go
index 2873ba7..d3c4766 100644
--- a/pkg/service/did/model.go
+++ b/pkg/service/did/model.go
@@ -5,6 +5,7 @@ import (
 
 	"github.com/TBD54566975/ssi-sdk/crypto"
 	didsdk "github.com/TBD54566975/ssi-sdk/did"
+	"github.com/TBD54566975/ssi-sdk/did/ion"
 	"github.com/TBD54566975/ssi-sdk/did/resolution"
 	"github.com/tbd54566975/ssi-service/pkg/service/common"
 )
@@ -84,3 +85,26 @@ type DeleteDIDRequest struct {
 	Method didsdk.Method `json:"method" validate:"required"`
 	ID     string        `json:"id" validate:"required"`
 }
+
+type UpdateIONDIDRequest struct {
+	DID ion.ION `json:"did"`
+
+	StateChange ion.StateChange `json:"stateChange"`
+}
+
+type UpdateIONDIDResponse struct {
+	DID didsdk.Document `json:"did"`
+}
+
+type UpdateRequestStatus string
+
+func (s UpdateRequestStatus) Bytes() []byte {
+	return []byte(s)
+}
+
+const (
+	PreAnchorStatus   UpdateRequestStatus = "pre-anchor"
+	AnchorErrorStatus UpdateRequestStatus = "anchor-error"
+	AnchoredStatus    UpdateRequestStatus = "anchored"
+	DoneStatus        UpdateRequestStatus = "done"
+)
diff --git a/pkg/service/did/service.go b/pkg/service/did/service.go
index 4108105..d3608bc 100644
--- a/pkg/service/did/service.go
+++ b/pkg/service/did/service.go
@@ -27,7 +27,9 @@ type Service struct {
 	resolver *resolution.ServiceResolver
 
 	// external dependencies
-	keyStore *keystore.Service
+	keyStore          *keystore.Service
+	keyStoreFactory   keystore.ServiceFactory
+	didStorageFactory StorageFactory
 }
 
 func (s *Service) Type() framework.Type {
@@ -66,17 +68,19 @@ func (s *Service) GetResolver() didresolution.Resolver {
 	return s.resolver
 }
 
-func NewDIDService(config config.DIDServiceConfig, s storage.ServiceStorage, keyStore *keystore.Service) (*Service, error) {
+func NewDIDService(config config.DIDServiceConfig, s storage.ServiceStorage, keyStore *keystore.Service, factory keystore.ServiceFactory) (*Service, error) {
 	didStorage, err := NewDIDStorage(s)
 	if err != nil {
 		return nil, errors.Wrap(err, "could not instantiate DID storage for the DID service")
 	}
 
 	service := Service{
-		config:   config,
-		storage:  didStorage,
-		handlers: make(map[didsdk.Method]MethodHandler),
-		keyStore: keyStore,
+		config:            config,
+		storage:           didStorage,
+		didStorageFactory: NewDIDStorageFactory(s),
+		handlers:          make(map[didsdk.Method]MethodHandler),
+		keyStore:          keyStore,
+		keyStoreFactory:   factory,
 	}
 
 	// instantiate all handlers for DID methods
@@ -122,7 +126,7 @@ func (s *Service) instantiateHandlerForMethod(method didsdk.Method) error {
 		}
 		s.handlers[method] = wh
 	case didsdk.IONMethod:
-		ih, err := NewIONHandler(s.Config().IONResolverURL, s.storage, s.keyStore)
+		ih, err := NewIONHandler(s.Config().IONResolverURL, s.storage, s.keyStore, s.keyStoreFactory, s.didStorageFactory)
 		if err != nil {
 			return errors.Wrap(err, "instantiating ion handler")
 		}
@@ -168,6 +172,18 @@ func (s *Service) CreateDIDByMethod(ctx context.Context, request CreateDIDReques
 	return handler.CreateDID(ctx, request)
 }
 
+func (s *Service) UpdateIONDID(ctx context.Context, request UpdateIONDIDRequest) (*UpdateIONDIDResponse, error) {
+	handler, err := s.getHandler(didsdk.IONMethod)
+	if err != nil {
+		return nil, sdkutil.LoggingErrorMsgf(err, "could not get handler for method<%s>", didsdk.IONMethod)
+	}
+	ionHandlerImpl, ok := handler.(*ionHandler)
+	if !ok {
+		return nil, errors.New("cannot assert that handler is an ionHandler")
+	}
+	return ionHandlerImpl.UpdateDID(ctx, request)
+}
+
 func (s *Service) GetDIDByMethod(ctx context.Context, request GetDIDRequest) (*GetDIDResponse, error) {
 	handler, err := s.getHandler(request.Method)
 	if err != nil {
diff --git a/pkg/service/service.go b/pkg/service/service.go
index 9183c39..ffabf35 100644
--- a/pkg/service/service.go
+++ b/pkg/service/service.go
@@ -106,7 +106,7 @@ func instantiateServices(config config.ServicesConfig) (*SSIService, error) {
 		return nil, sdkutil.LoggingErrorMsg(err, "could not instantiate batch DID service")
 	}
 
-	didService, err := did.NewDIDService(config.DIDConfig, storageProvider, keyStoreService)
+	didService, err := did.NewDIDService(config.DIDConfig, storageProvider, keyStoreService, keyStoreServiceFactory)
 	if err != nil {
 		return nil, sdkutil.LoggingErrorMsg(err, "could not instantiate the DID service")
 	}


Test patch:
diff --git a/integration/didion_integration_test.go b/integration/didion_integration_test.go
index 11778f8..249f5cb 100644
--- a/integration/didion_integration_test.go
+++ b/integration/didion_integration_test.go
@@ -6,7 +6,6 @@ import (
 	"github.com/TBD54566975/ssi-sdk/crypto"
 	"github.com/TBD54566975/ssi-sdk/did/key"
 	"github.com/stretchr/testify/assert"
-
 	"github.com/tbd54566975/ssi-service/pkg/service/operation/storage"
 )
 
@@ -68,6 +67,23 @@ func TestCreateIssuerDIDIONIntegration(t *testing.T) {
 	assert.Equal(t, "test-kid", verificationMethod2KID)
 }
 
+func TestUpdateDIDIONIntegration(t *testing.T) {
+	if testing.Short() {
+		t.Skip("skipping integration test")
+	}
+	didIONOutput, err := CreateDIDION()
+	assert.NoError(t, err)
+
+	issuerDID, err := getJSONElement(didIONOutput, "$.did.id")
+	assert.NoError(t, err)
+	assert.Contains(t, issuerDID, "did:ion")
+
+	// Because ION nodes do not allow updates immediately after creation of a DID, we expect the following error code.
+	_, err = UpdateDIDION(issuerDID)
+	assert.Error(t, err)
+	assert.ErrorContains(t, err, "queueing_multiple_operations_per_did_not_allowed")
+}
+
 func TestCreateAliceDIDKeyForDIDIONIntegration(t *testing.T) {
 	if testing.Short() {
 		t.Skip("skipping integration test")
diff --git a/integration/testdata/update-did-ion-input.json b/integration/testdata/update-did-ion-input.json
new file mode 100644
index 0000000..ffc301e
--- /dev/null
+++ b/integration/testdata/update-did-ion-input.json
@@ -0,0 +1,29 @@
+{
+  "stateChange": {
+    "publicKeysToAdd": [
+      {
+        "id": "publicKeyModel1Id",
+        "type": "JsonWebKey2020",
+        "publicKeyJwk": {
+          "kty": "EC",
+          "crv": "secp256k1",
+          "x": "Z4Y3NNOxv0J6tCgqOBFnHnaZhJF6LdulT7z8A-2D5_8",
+          "y": "i5a2NtJoUKXkLm6q8nOEu9WOkso1Ag6FTUT6k_LMnGk"
+        },
+        "purposes": [
+          "authentication"
+        ]
+      },
+      {
+        "id": "publicKeyModel2Id",
+        "type": "JsonWebKey2020",
+        "publicKeyJwk": {
+          "kty": "OKP",
+          "crv": "Ed25519",
+          "x": "VCpo2LMLhn6iWku8MKvSLg2ZAoC-nlOyPVQaO3FxVeQ"
+        },
+        "purposes": ["keyAgreement"]
+      }
+    ]
+  }
+}
\ No newline at end of file
diff --git a/pkg/server/router/did_test.go b/pkg/server/router/did_test.go
index 6a2c0d2..41f5f18 100644
--- a/pkg/server/router/did_test.go
+++ b/pkg/server/router/did_test.go
@@ -42,7 +42,7 @@ func TestDIDRouter(t *testing.T) {
 					keyStoreService := testKeyStoreService(tt, db)
 					methods := []string{didsdk.KeyMethod.String()}
 					serviceConfig := config.DIDServiceConfig{Methods: methods, LocalResolutionMethods: methods}
-					didService, err := did.NewDIDService(serviceConfig, db, keyStoreService)
+					didService, err := did.NewDIDService(serviceConfig, db, keyStoreService, nil)
 					assert.NoError(tt, err)
 					assert.NotEmpty(tt, didService)
 					createDID(tt, didService)
@@ -84,7 +84,7 @@ func TestDIDRouter(t *testing.T) {
 				keyStoreService := testKeyStoreService(tt, db)
 				methods := []string{didsdk.KeyMethod.String()}
 				serviceConfig := config.DIDServiceConfig{Methods: methods, LocalResolutionMethods: methods}
-				didService, err := did.NewDIDService(serviceConfig, db, keyStoreService)
+				didService, err := did.NewDIDService(serviceConfig, db, keyStoreService, nil)
 				assert.NoError(tt, err)
 				assert.NotEmpty(tt, didService)
 
@@ -171,7 +171,7 @@ func TestDIDRouter(t *testing.T) {
 				keyStoreService := testKeyStoreService(tt, db)
 				methods := []string{didsdk.KeyMethod.String(), didsdk.WebMethod.String()}
 				serviceConfig := config.DIDServiceConfig{Methods: methods, LocalResolutionMethods: methods}
-				didService, err := did.NewDIDService(serviceConfig, db, keyStoreService)
+				didService, err := did.NewDIDService(serviceConfig, db, keyStoreService, nil)
 				assert.NoError(tt, err)
 				assert.NotEmpty(tt, didService)
 
diff --git a/pkg/server/router/testutils_test.go b/pkg/server/router/testutils_test.go
index 71d5b8b..dda9ae0 100644
--- a/pkg/server/router/testutils_test.go
+++ b/pkg/server/router/testutils_test.go
@@ -46,7 +46,7 @@ func testDIDService(t *testing.T, db storage.ServiceStorage, keyStore *keystore.
 		LocalResolutionMethods: []string{"key"},
 	}
 	// create a did service
-	didService, err := did.NewDIDService(serviceConfig, db, keyStore)
+	didService, err := did.NewDIDService(serviceConfig, db, keyStore, nil)
 	require.NoError(t, err)
 	require.NotEmpty(t, didService)
 	return didService
diff --git a/pkg/server/server_did_test.go b/pkg/server/server_did_test.go
index 2a2aa46..32c71c2 100644
--- a/pkg/server/server_did_test.go
+++ b/pkg/server/server_did_test.go
@@ -10,7 +10,9 @@ import (
 	"testing"
 
 	"github.com/TBD54566975/ssi-sdk/crypto"
+	"github.com/TBD54566975/ssi-sdk/crypto/jwx"
 	didsdk "github.com/TBD54566975/ssi-sdk/did"
+	"github.com/TBD54566975/ssi-sdk/did/ion"
 	"github.com/goccy/go-json"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -178,7 +180,7 @@ func TestDIDAPI(t *testing.T) {
 				require.NotEmpty(tt, db)
 
 				_, keyStoreService, _ := testKeyStore(tt, db)
-				didService, _ := testDIDRouter(tt, db, keyStoreService, []string{"ion"}, nil)
+				didRouter, _ := testDIDRouter(tt, db, keyStoreService, []string{"ion"}, nil)
 
 				// create DID by method - ion - missing body
 				req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/ion", nil)
@@ -188,7 +190,7 @@ func TestDIDAPI(t *testing.T) {
 				}
 
 				c := newRequestContextWithParams(w, req, params)
-				didService.CreateDIDByMethod(c)
+				didRouter.CreateDIDByMethod(c)
 				assert.Contains(tt, w.Body.String(), "invalid create DID request")
 
 				// reset recorder between calls
@@ -206,7 +208,7 @@ func TestDIDAPI(t *testing.T) {
 				req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/ion", requestReader)
 
 				c = newRequestContextWithParams(w, req, params)
-				didService.CreateDIDByMethod(c)
+				didRouter.CreateDIDByMethod(c)
 				assert.True(tt, util.Is2xxResponse(w.Code))
 
 				// reset recorder between calls
@@ -221,7 +223,7 @@ func TestDIDAPI(t *testing.T) {
 				req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/ion", requestReader)
 
 				c = newRequestContextWithParams(w, req, params)
-				didService.CreateDIDByMethod(c)
+				didRouter.CreateDIDByMethod(c)
 				assert.Contains(tt, w.Body.String(), "could not create DID for method<ion> with key type: bad")
 
 				// reset recorder between calls
@@ -239,16 +241,140 @@ func TestDIDAPI(t *testing.T) {
 					Post("/operations").
 					Reply(200).
 					BodyString(string(BasicDIDResolution))
-				defer gock.Off()
 
 				c = newRequestContextWithParams(w, req, params)
-				didService.CreateDIDByMethod(c)
+				didRouter.CreateDIDByMethod(c)
 				assert.True(tt, util.Is2xxResponse(w.Code))
 
 				var resp router.CreateDIDByMethodResponse
 				err := json.NewDecoder(w.Body).Decode(&resp)
 				assert.NoError(tt, err)
 				assert.Contains(tt, resp.DID.ID, didsdk.IONMethod)
+
+				gock.Off()
+				// good params
+				req = httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids/ion/"+resp.DID.ID, nil)
+				goodParams := map[string]string{
+					"method": "ion",
+					"id":     resp.DID.ID,
+				}
+				c = newRequestContextWithParams(w, req, goodParams)
+				didRouter.GetDIDByMethod(c)
+				assert.True(tt, util.Is2xxResponse(w.Code))
+
+				var getDIDResp router.GetDIDByMethodResponse
+				err = json.NewDecoder(w.Body).Decode(&getDIDResp)
+				assert.NoError(tt, err)
+				assert.Equal(tt, resp.DID.ID, getDIDResp.DID.ID)
+			})
+
+			t.Run("Test Update DID By Method: ION", func(tt *testing.T) {
+				db := test.ServiceStorage(t)
+				require.NotEmpty(tt, db)
+
+				_, keyStoreService, keyStoreServiceFactory := testKeyStore(tt, db)
+				didService, _ := testDIDRouter(tt, db, keyStoreService, []string{"ion"}, keyStoreServiceFactory)
+
+				params := map[string]string{
+					"method": "ion",
+				}
+				// reset recorder between calls
+				w := httptest.NewRecorder()
+
+				gock.New(testIONResolverURL).
+					Post("/operations").
+					Reply(200).
+					JSON(string(BasicDIDResolution))
+				defer gock.Off()
+
+				// with body, good key type, no options
+				createDIDRequest := router.CreateDIDByMethodRequest{KeyType: crypto.Ed25519}
+				requestReader := newRequestValue(tt, createDIDRequest)
+				req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/ion", requestReader)
+
+				c := newRequestContextWithParams(w, req, params)
+				didService.CreateDIDByMethod(c)
+				assert.True(tt, util.Is2xxResponse(w.Code))
+
+				var createDIDResponse router.CreateDIDByMethodResponse
+				err := json.NewDecoder(w.Body).Decode(&createDIDResponse)
+				assert.NoError(tt, err)
+
+				updateDIDRequest := router.UpdateDIDByMethodRequest{
+					StateChange: router.StateChange{
+						PublicKeysToAdd: []ion.PublicKey{
+							{
+								ID:   "testPublicKeyModel1Id",
+								Type: "EcdsaSecp256k1VerificationKey2019",
+								PublicKeyJWK: jwx.PublicKeyJWK{
+									KTY: "EC",
+									CRV: "secp256k1",
+									X:   "tXSKB_rubXS7sCjXqupVJEzTcW3MsjmEvq1YpXn96Zg",
+									Y:   "dOicXqbjFxoGJ-K0-GJ1kHYJqic_D_OMuUwkQ7Ol6nk",
+								},
+								Purposes: []ion.PublicKeyPurpose{
+									ion.Authentication, ion.KeyAgreement,
+								},
+							},
+						},
+					},
+				}
+				w = httptest.NewRecorder()
+				params["id"] = createDIDResponse.DID.ID
+				requestReader = newRequestValue(tt, updateDIDRequest)
+				req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/ion/"+createDIDResponse.DID.ID, requestReader)
+
+				gock.New(testIONResolverURL).
+					Post("/operations").
+					Reply(200).
+					JSON("{}")
+				defer gock.Off()
+
+				c = newRequestContextWithParams(w, req, params)
+				didService.UpdateDIDByMethod(c)
+				assert.True(tt, util.Is2xxResponse(w.Code))
+
+				updateDIDRequest2 := router.UpdateDIDByMethodRequest{
+					StateChange: router.StateChange{
+						ServicesToAdd: []didsdk.Service{
+							{
+								ID:              "testService1Id",
+								Type:            "testService1Type",
+								ServiceEndpoint: "http://www.service1.com",
+							},
+						},
+					},
+				}
+				w = httptest.NewRecorder()
+				requestReader = newRequestValue(tt, updateDIDRequest2)
+				req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/ion/"+createDIDResponse.DID.ID, requestReader)
+
+				gock.New(testIONResolverURL).
+					Post("/operations").
+					Reply(200).
+					JSON("{}")
+				defer gock.Off()
+
+				c = newRequestContextWithParams(w, req, params)
+				didService.UpdateDIDByMethod(c)
+				assert.True(tt, util.Is2xxResponse(w.Code))
+
+				var updateDIDResponse router.UpdateDIDByMethodResponse
+				err = json.NewDecoder(w.Body).Decode(&updateDIDResponse)
+				assert.NoError(tt, err)
+				assert.Contains(tt, updateDIDResponse.DID.ID, didsdk.IONMethod)
+
+				assert.Equal(tt, "#testService1Id", updateDIDResponse.DID.Services[0].ID)
+				// Check that the correct updates were performed.
+				assert.Len(tt, updateDIDResponse.DID.VerificationMethod, 1+len(createDIDResponse.DID.VerificationMethod))
+				assert.Equal(tt, createDIDResponse.DID.VerificationMethod[0].ID, updateDIDResponse.DID.VerificationMethod[0].ID)
+				assert.Equal(tt, "#testPublicKeyModel1Id", updateDIDResponse.DID.VerificationMethod[1].ID)
+				assert.Len(tt, updateDIDResponse.DID.Authentication, 1+len(createDIDResponse.DID.Authentication))
+				assert.Len(tt, updateDIDResponse.DID.AssertionMethod, 0+len(createDIDResponse.DID.AssertionMethod))
+				assert.Len(tt, updateDIDResponse.DID.KeyAgreement, 1+len(createDIDResponse.DID.KeyAgreement))
+				assert.Len(tt, updateDIDResponse.DID.CapabilityInvocation, 0+len(createDIDResponse.DID.CapabilityInvocation))
+				assert.Len(tt, updateDIDResponse.DID.CapabilityInvocation, 0+len(createDIDResponse.DID.CapabilityInvocation))
+
 			})
 
 			t.Run("Test Create Duplicate DID:Webs", func(tt *testing.T) {
diff --git a/pkg/server/server_test.go b/pkg/server/server_test.go
index 00b5fc4..f3b6246 100644
--- a/pkg/server/server_test.go
+++ b/pkg/server/server_test.go
@@ -243,7 +243,7 @@ func testDIDService(t *testing.T, bolt storage.ServiceStorage, keyStore *keystor
 	}
 
 	// create a did service
-	didService, err := did.NewDIDService(serviceConfig, bolt, keyStore)
+	didService, err := did.NewDIDService(serviceConfig, bolt, keyStore, factory)
 	require.NoError(t, err)
 	require.NotEmpty(t, didService)
 
diff --git a/pkg/service/did/ion_test.go b/pkg/service/did/ion_test.go
index 1a73231..f938574 100644
--- a/pkg/service/did/ion_test.go
+++ b/pkg/service/did/ion_test.go
@@ -28,7 +28,7 @@ func TestIONHandler(t *testing.T) {
 	for _, test := range testutil.TestDatabases {
 		t.Run(test.Name, func(t *testing.T) {
 			t.Run("Create ION Handler", func(tt *testing.T) {
-				handler, err := NewIONHandler("", nil, nil)
+				handler, err := NewIONHandler("", nil, nil, nil, nil)
 				assert.Error(tt, err)
 				assert.Empty(tt, handler)
 				assert.Contains(tt, err.Error(), "baseURL cannot be empty")
@@ -37,22 +37,22 @@ func TestIONHandler(t *testing.T) {
 				keystoreService := testKeyStoreService(tt, s)
 				didStorage, err := NewDIDStorage(s)
 				assert.NoError(tt, err)
-				handler, err = NewIONHandler("bad", nil, keystoreService)
+				handler, err = NewIONHandler("bad", nil, keystoreService, nil, nil)
 				assert.Error(tt, err)
 				assert.Empty(tt, handler)
 				assert.Contains(tt, err.Error(), "storage cannot be empty")
 
-				handler, err = NewIONHandler("bad", didStorage, nil)
+				handler, err = NewIONHandler("bad", didStorage, nil, nil, nil)
 				assert.Error(tt, err)
 				assert.Empty(tt, handler)
 				assert.Contains(tt, err.Error(), "keystore cannot be empty")
 
-				handler, err = NewIONHandler("bad", didStorage, keystoreService)
+				handler, err = NewIONHandler("bad", didStorage, keystoreService, nil, nil)
 				assert.Error(tt, err)
 				assert.Empty(tt, handler)
 				assert.Contains(tt, err.Error(), "invalid resolution URL")
 
-				handler, err = NewIONHandler("https://example.com", didStorage, keystoreService)
+				handler, err = NewIONHandler("https://example.com", didStorage, keystoreService, nil, nil)
 				assert.NoError(tt, err)
 				assert.NotEmpty(tt, handler)
 
@@ -65,7 +65,7 @@ func TestIONHandler(t *testing.T) {
 				keystoreService := testKeyStoreService(tt, s)
 				didStorage, err := NewDIDStorage(s)
 				assert.NoError(tt, err)
-				handler, err := NewIONHandler("https://test-ion-resolver.com", didStorage, keystoreService)
+				handler, err := NewIONHandler("https://test-ion-resolver.com", didStorage, keystoreService, nil, nil)
 				assert.NoError(tt, err)
 				assert.NotEmpty(tt, handler)
 
@@ -137,7 +137,7 @@ func TestIONHandler(t *testing.T) {
 				keystoreService := testKeyStoreService(tt, s)
 				didStorage, err := NewDIDStorage(s)
 				assert.NoError(tt, err)
-				handler, err := NewIONHandler("https://ion.tbddev.org", didStorage, keystoreService)
+				handler, err := NewIONHandler("https://ion.tbddev.org", didStorage, keystoreService, nil, nil)
 				assert.NoError(tt, err)
 				assert.NotEmpty(tt, handler)
 
@@ -164,7 +164,7 @@ func TestIONHandler(t *testing.T) {
 				keystoreService := testKeyStoreService(tt, s)
 				didStorage, err := NewDIDStorage(s)
 				assert.NoError(tt, err)
-				handler, err := NewIONHandler("https://test-ion-resolver.com", didStorage, keystoreService)
+				handler, err := NewIONHandler("https://test-ion-resolver.com", didStorage, keystoreService, nil, nil)
 				assert.NoError(tt, err)
 				assert.NotEmpty(tt, handler)
 
@@ -203,7 +203,7 @@ func TestIONHandler(t *testing.T) {
 				keystoreService := testKeyStoreService(tt, s)
 				didStorage, err := NewDIDStorage(s)
 				assert.NoError(tt, err)
-				handler, err := NewIONHandler("https://test-ion-resolver.com", didStorage, keystoreService)
+				handler, err := NewIONHandler("https://test-ion-resolver.com", didStorage, keystoreService, nil, nil)
 				assert.NoError(tt, err)
 				assert.NotEmpty(tt, handler)
 
@@ -261,7 +261,7 @@ func TestIONHandler(t *testing.T) {
 				keystoreService := testKeyStoreService(tt, s)
 				didStorage, err := NewDIDStorage(s)
 				assert.NoError(tt, err)
-				handler, err := NewIONHandler("https://test-ion-resolver.com", didStorage, keystoreService)
+				handler, err := NewIONHandler("https://test-ion-resolver.com", didStorage, keystoreService, nil, nil)
 				assert.NoError(tt, err)
 				assert.NotEmpty(tt, handler)