Skip to content

Commit

Permalink
Support adding/removing services and keys to an ion DID (#562)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
andresuribe87 authored Aug 3, 2023
1 parent f1b0954 commit 028dbf9
Show file tree
Hide file tree
Showing 15 changed files with 817 additions and 58 deletions.
102 changes: 102 additions & 0 deletions doc/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
9 changes: 9 additions & 0 deletions integration/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand Down
18 changes: 17 additions & 1 deletion integration/didion_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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")
Expand Down
29 changes: 29 additions & 0 deletions integration/testdata/update-did-ion-input.json
Original file line number Diff line number Diff line change
@@ -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"]
}
]
}
}
95 changes: 95 additions & 0 deletions pkg/server/router/did.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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{
Expand Down
6 changes: 3 additions & 3 deletions pkg/server/router/did_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion pkg/server/router/testutils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions pkg/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading

0 comments on commit 028dbf9

Please sign in to comment.