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 2 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 @@ -807,6 +807,33 @@ definitions:
- urls
- verb
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 @@ -1718,6 +1745,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 @@ -1785,6 +1831,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 @@ -2500,6 +2561,47 @@ paths:
summary: Get DID
tags:
- DecentralizedIdentityAPI
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:
"201":
description: Created
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/resolver/{id}:
get:
consumes:
Expand Down
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 201 {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
}

createDIDRequest, err := toUpdateDIDRequest(*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
}
createDIDResponse, err := dr.service.UpdateIONDID(c, *createDIDRequest)
if err != nil {
errMsg := fmt.Sprintf("could not create DID for method<%s>", *method)
Copy link
Contributor

Choose a reason for hiding this comment

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

is this the right error message?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

framework.LoggingRespondErrWithMsg(c, err, errMsg, http.StatusInternalServerError)
return
}

resp := CreateDIDByMethodResponse{DID: createDIDResponse.DID}
framework.Respond(c, resp, http.StatusCreated)

}

func toUpdateDIDRequest(id string, request UpdateDIDByMethodRequest) (*did.UpdateDIDRequest, 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.UpdateDIDRequest{
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
1 change: 1 addition & 0 deletions pkg/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ func DecentralizedIdentityAPI(rg *gin.RouterGroup, service *didsvc.Service, webh
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.GET("/:method", didRouter.ListDIDsByMethod)
didAPI.GET("/:method/:id", didRouter.GetDIDByMethod)
didAPI.DELETE("/:method/:id", didRouter.SoftDeleteDIDByMethod)
Expand Down
108 changes: 108 additions & 0 deletions pkg/server/server_did_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,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"
Expand Down Expand Up @@ -245,6 +247,112 @@ func TestDIDAPI(t *testing.T) {
assert.Contains(tt, resp.DID.ID, didsdk.IONMethod)
})

t.Run("Test Update DID By Method: ION", func(tt *testing.T) {
db := test.ServiceStorage(t)
require.NotEmpty(tt, db)

_, keyStoreService := testKeyStore(tt, db)
didService := testDIDRouter(tt, db, keyStoreService, []string{"ion"})

params := map[string]string{
"method": "ion",
}
// reset recorder between calls
w := httptest.NewRecorder()

gock.New(testIONResolverURL).
Post("/operations").
Reply(200)
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: "publicKeyModel1Id",
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)
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: "service1Id",
Type: "service1Type",
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)
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, "service1Id", 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], updateDIDResponse.DID.VerificationMethod[0])
assert.Equal(tt, "publicKeyModel1Id", 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) {
db := test.ServiceStorage(t)
require.NotEmpty(tt, db)
Expand Down
Loading