Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support adding/removing services and keys to an ion DID #562

Merged
merged 16 commits into from
Aug 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this works until we support update for other methods, then we'll probably want to reconsider this API

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. Since we don't have other examples, I think this makes sense for now.

}

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() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this logic is better suited for the service layer, though it is OK to duplicate it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The service layer method is called UpdateIONDID, to which you pass in an UpdateIONDIDRequest object. By design, it's not possible to call that method without an ION did.

The external facing API is open for evolution. The internal is specific so it's more clear and it's easier to maintain.

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)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could check to see whether any of the updates are set to make sure it's not a no-op

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this validation to ionHandler.UpdateDID. Could validate in both places, but the ionHandler one seems more appropriate in case other endpoints end up calling that method.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds good

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
Loading