From 028dbf93be843864a9db56e4375d85bff148ca87 Mon Sep 17 00:00:00 2001 From: Andres Uribe Date: Thu, 3 Aug 2023 17:50:46 -0400 Subject: [PATCH] Support adding/removing services and keys to an ion DID (#562) * Support adding/removing services and keys to an ion DID * Reviews * Fixes 560 where multiple instances of the service used different encryption keys * Factories, refactoring, and integration test. * Move to actually dealing with patches. * spec * 200 and spec * Moving around bits --- doc/swagger.yaml | 102 +++++ integration/common.go | 9 + integration/didion_integration_test.go | 18 +- .../testdata/update-did-ion-input.json | 29 ++ pkg/server/router/did.go | 95 +++++ pkg/server/router/did_test.go | 6 +- pkg/server/router/testutils_test.go | 2 +- pkg/server/server.go | 1 + pkg/server/server_did_test.go | 138 +++++- pkg/server/server_test.go | 2 +- pkg/service/did/ion.go | 397 ++++++++++++++++-- pkg/service/did/ion_test.go | 20 +- pkg/service/did/model.go | 24 ++ pkg/service/did/service.go | 30 +- pkg/service/service.go | 2 +- 15 files changed, 817 insertions(+), 58 deletions(-) create mode 100644 integration/testdata/update-did-ion-input.json diff --git a/doc/swagger.yaml b/doc/swagger.yaml index f750e439c..93b92bb0c 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 09628ce52..3070a9dc3 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/integration/didion_integration_test.go b/integration/didion_integration_test.go index 11778f8b2..249f5cbff 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 000000000..ffc301e94 --- /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.go b/pkg/server/router/did.go index 28cddeeab..208ee4744 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/router/did_test.go b/pkg/server/router/did_test.go index 6a2c0d23b..41f5f18a6 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 71d5b8b89..dda9ae049 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.go b/pkg/server/server.go index dbc760428..6e3bb2824 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/server/server_did_test.go b/pkg/server/server_did_test.go index 2a2aa4636..32c71c23e 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 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 00b5fc422..f3b624617 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.go b/pkg/service/did/ion.go index f817defaa..ceb83d26a 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/ion_test.go b/pkg/service/did/ion_test.go index 1a7323112..f9385748e 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) diff --git a/pkg/service/did/model.go b/pkg/service/did/model.go index 2873ba740..d3c4766f6 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 4108105a2..d3608bcc7 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 9183c393e..ffabf3579 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") }