Skip to content
This repository has been archived by the owner on Dec 12, 2024. It is now read-only.

Commit

Permalink
Adding credential revocation (#169)
Browse files Browse the repository at this point in the history
* Adding credential revocation

* updating config and refactor

* remove suspended

Co-authored-by: Gabe <[email protected]>
  • Loading branch information
nitro-neal and Gabe authored Nov 15, 2022
1 parent 114818b commit 3723d02
Show file tree
Hide file tree
Showing 14 changed files with 1,056 additions and 22 deletions.
1 change: 1 addition & 0 deletions config/compose.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ log_level = "info"

[services]
storage = "bolt"
service_endpoint = "http://localhost:8000"

# per-service configuration
[services.keystore]
Expand Down
17 changes: 14 additions & 3 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ const (
ConfigFileName = "config.toml"
ServiceName = "ssi-service"
ConfigExtension = ".toml"

DefaultServiceEndpoint = "http://localhost:8000"
)

type SSIServiceConfig struct {
Expand Down Expand Up @@ -45,9 +47,9 @@ type ServicesConfig struct {
// in the future it may make sense to have per-service storage providers (e.g. mysql for one service,
// mongo for another)
StorageProvider string `toml:"storage"`
ServiceEndpoint string `toml:"service_endpoint"`

// Embed all service-specific configs here. The order matters: from which should be instantiated first, to last

KeyStoreConfig KeyStoreServiceConfig `toml:"keystore,omitempty"`
DIDConfig DIDServiceConfig `toml:"did,omitempty"`
SchemaConfig SchemaServiceConfig `toml:"schema,omitempty"`
Expand All @@ -59,7 +61,8 @@ type ServicesConfig struct {
// BaseServiceConfig represents configurable properties for a specific component of the SSI Service
// Can be wrapped and extended for any specific service config
type BaseServiceConfig struct {
Name string `toml:"name"`
Name string `toml:"name"`
ServiceEndpoint string `toml:"service_endpoint"`
}

type KeyStoreServiceConfig struct {
Expand Down Expand Up @@ -102,6 +105,7 @@ func (s *SchemaServiceConfig) IsEmpty() bool {

type CredentialServiceConfig struct {
*BaseServiceConfig

// TODO(gabe) supported key and signature types
}

Expand Down Expand Up @@ -177,6 +181,7 @@ func LoadConfig(path string) (*SSIServiceConfig, error) {
if defaultConfig {
config.Services = ServicesConfig{
StorageProvider: "bolt",
ServiceEndpoint: DefaultServiceEndpoint,
KeyStoreConfig: KeyStoreServiceConfig{
BaseServiceConfig: &BaseServiceConfig{Name: "keystore"},
ServiceKeyPassword: "default-password",
Expand All @@ -190,7 +195,7 @@ func LoadConfig(path string) (*SSIServiceConfig, error) {
BaseServiceConfig: &BaseServiceConfig{Name: "schema"},
},
CredentialConfig: CredentialServiceConfig{
BaseServiceConfig: &BaseServiceConfig{Name: "credential"},
BaseServiceConfig: &BaseServiceConfig{Name: "credential", ServiceEndpoint: DefaultServiceEndpoint},
},
ManifestConfig: ManifestServiceConfig{
BaseServiceConfig: &BaseServiceConfig{Name: "manifest"},
Expand All @@ -204,6 +209,12 @@ func LoadConfig(path string) (*SSIServiceConfig, error) {
if _, err := toml.DecodeFile(path, &config); err != nil {
return nil, errors.Wrapf(err, "could not load config: %s", path)
}

// apply defaults if not included in toml file
if config.Services.CredentialConfig.BaseServiceConfig.ServiceEndpoint == "" {
config.Services.CredentialConfig.BaseServiceConfig.ServiceEndpoint = config.Services.ServiceEndpoint
}

}

return &config, nil
Expand Down
1 change: 1 addition & 0 deletions config/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ log_level = "debug"

[services]
storage = "bolt"
service_endpoint = "http://localhost:8000"

# per-service configuration
[services.keystore]
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ require (
)

require (
github.com/bits-and-blooms/bitset v1.3.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect
github.com/go-logr/logr v1.2.3 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ github.com/TBD54566975/ssi-sdk v0.0.2-alpha.0.20221110170444-a9e67907c8f9 h1:46X
github.com/TBD54566975/ssi-sdk v0.0.2-alpha.0.20221110170444-a9e67907c8f9/go.mod h1:c2wq4GipGxjAPXgnxNBvnuB7OqDNKjriKuD60H0Q6Ho=
github.com/ardanlabs/conf v1.5.0 h1:5TwP6Wu9Xi07eLFEpiCUF3oQXh9UzHMDVnD3u/I5d5c=
github.com/ardanlabs/conf v1.5.0/go.mod h1:ILsMo9dMqYzCxDjDXTiwMI0IgxOJd0MOiucbQY2wlJw=
github.com/bits-and-blooms/bitset v1.3.3 h1:R1XWiopGiXf66xygsiLpzLo67xEYvMkHw3w+rCOSAwg=
github.com/bits-and-blooms/bitset v1.3.3/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
Expand Down
1 change: 1 addition & 0 deletions internal/credential/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type Container struct {
ID string
Credential *credential.VerifiableCredential
CredentialJWT *keyaccess.JWT
Revoked bool
}

func (c Container) JWTString() string {
Expand Down
148 changes: 145 additions & 3 deletions pkg/server/router/credential.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,10 @@ type CreateCredentialRequest struct {
// A context is optional. If not present, we'll apply default, required context values.
Context string `json:"@context"`
// A schema is optional. If present, we'll attempt to look it up and validate the data against it.
Schema string `json:"schema"`
Data map[string]interface{} `json:"data" validate:"required"`
Expiry string `json:"expiry"`
Schema string `json:"schema"`
Data map[string]interface{} `json:"data" validate:"required"`
Expiry string `json:"expiry"`
Revocable bool `json:"revocable"`
// TODO(gabe) support more capabilities like signature type, format, status, and more.
}

Expand All @@ -60,6 +61,7 @@ func (c CreateCredentialRequest) ToServiceRequest() credential.CreateCredentialR
JSONSchema: c.Schema,
Data: c.Data,
Expiry: c.Expiry,
Revocable: c.Revocable,
}
}

Expand Down Expand Up @@ -145,6 +147,146 @@ func (cr CredentialRouter) GetCredential(ctx context.Context, w http.ResponseWri
return framework.Respond(ctx, w, resp, http.StatusOK)
}

type GetCredentialStatusResponse struct {
Revoked bool `json:"revoked"`
}

// GetCredentialStatus godoc
// @Summary Get Credential Status
// @Description Get credential status by id
// @Tags CredentialAPI
// @Accept json
// @Produce json
// @Param id path string true "ID"
// @Success 200 {object} GetCredentialStatusResponse
// @Failure 400 {string} string "Bad request"
// @Router /v1/credentials/{id}/status [get]
func (cr CredentialRouter) GetCredentialStatus(ctx context.Context, w http.ResponseWriter, _ *http.Request) error {
id := framework.GetParam(ctx, IDParam)
if id == nil {
errMsg := "cannot get credential without ID parameter"
logrus.Error(errMsg)
return framework.NewRequestErrorMsg(errMsg, http.StatusBadRequest)
}

gotCredential, err := cr.service.GetCredential(credential.GetCredentialRequest{ID: *id})
if err != nil {
errMsg := fmt.Sprintf("could not get credential with id: %s", *id)
logrus.WithError(err).Error(errMsg)
return framework.NewRequestError(errors.Wrap(err, errMsg), http.StatusBadRequest)
}

resp := GetCredentialStatusResponse{
Revoked: gotCredential.Revoked,
}

return framework.Respond(ctx, w, resp, http.StatusOK)
}

type GetCredentialStatusListResponse struct {
ID string `json:"id"`
Credential *credsdk.VerifiableCredential `json:"credential,omitempty"`
CredentialJWT *keyaccess.JWT `json:"credentialJwt,omitempty"`
}

// GetCredentialStatusList godoc
// @Summary Get Credential Status List
// @Description Get credential status list by id
// @Tags CredentialAPI
// @Accept json
// @Produce json
// @Param id path string true "ID"
// @Success 200 {object} GetCredentialStatusListResponse
// @Failure 400 {string} string "Bad request"
// @Router /v1/credentials/status/{id} [get]
func (cr CredentialRouter) GetCredentialStatusList(ctx context.Context, w http.ResponseWriter, _ *http.Request) error {
id := framework.GetParam(ctx, IDParam)
if id == nil {
errMsg := "cannot get credential without ID parameter"
logrus.Error(errMsg)
return framework.NewRequestErrorMsg(errMsg, http.StatusBadRequest)
}

gotCredential, err := cr.service.GetCredentialStatusList(credential.GetCredentialStatusListRequest{ID: *id})
if err != nil {
errMsg := fmt.Sprintf("could not get credential status list with id: %s", *id)
logrus.WithError(err).Error(errMsg)
return framework.NewRequestError(errors.Wrap(err, errMsg), http.StatusBadRequest)
}

resp := GetCredentialStatusListResponse{
ID: gotCredential.ID,
Credential: gotCredential.Credential,
CredentialJWT: gotCredential.CredentialJWT,
}

return framework.Respond(ctx, w, resp, http.StatusOK)
}

type UpdateCredentialStatusRequest struct {
Revoked bool `json:"revoked" validate:"required"`
}

func (c UpdateCredentialStatusRequest) ToServiceRequest(id string) credential.UpdateCredentialStatusRequest {
return credential.UpdateCredentialStatusRequest{
ID: id,
Revoked: c.Revoked,
}
}

type UpdateCredentialStatusResponse struct {
Revoked bool `json:"revoked"`
}

// UpdateCredentialStatus godoc
// @Summary Update Credential Status
// @Description Update a credential's status
// @Tags CredentialAPI
// @Accept json
// @Produce json
// @Param request body UpdateCredentialStatusRequest true "request body"
// @Success 201 {object} UpdateCredentialStatusResponse
// @Failure 400 {string} string "Bad request"
// @Failure 500 {string} string "Internal server error"
// @Router /v1/credentials/{id}/status [put]
func (cr CredentialRouter) UpdateCredentialStatus(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
id := framework.GetParam(ctx, IDParam)
if id == nil {
errMsg := "cannot get credential without ID parameter"
logrus.Error(errMsg)
return framework.NewRequestErrorMsg(errMsg, http.StatusBadRequest)
}

var request UpdateCredentialStatusRequest
invalidCreateCredentialRequest := "invalid update credential request"
if err := framework.Decode(r, &request); err != nil {
errMsg := invalidCreateCredentialRequest
logrus.WithError(err).Error(errMsg)
return framework.NewRequestError(errors.Wrap(err, errMsg), http.StatusBadRequest)
}

if err := framework.ValidateRequest(request); err != nil {
errMsg := invalidCreateCredentialRequest
logrus.WithError(err).Error(errMsg)
return framework.NewRequestError(errors.Wrap(err, errMsg), http.StatusBadRequest)
}

req := request.ToServiceRequest(*id)
gotCredential, err := cr.service.UpdateCredentialStatus(req)

if err != nil {
errMsg := fmt.Sprintf("could not update credential with id: %s", req.ID)
logrus.WithError(err).Error(errMsg)
return framework.NewRequestError(errors.Wrap(err, errMsg), http.StatusBadRequest)
}

resp := UpdateCredentialStatusResponse{
Revoked: gotCredential.Revoked,
}

return framework.Respond(ctx, w, resp, http.StatusOK)
}

type VerifyCredentialRequest struct {
DataIntegrityCredential *credsdk.VerifiableCredential `json:"credential,omitempty"`
CredentialJWT *keyaccess.JWT `json:"credentialJwt,omitempty"`
Expand Down
100 changes: 100 additions & 0 deletions pkg/server/router/credential_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,17 @@ func TestCredentialRouter(t *testing.T) {
})

t.Run("Credential Service Test", func(tt *testing.T) {

bolt, err := storage.NewBoltDB()
assert.NoError(tt, err)
assert.NotEmpty(tt, bolt)

// remove the db file after the test
tt.Cleanup(func() {
_ = bolt.Close()
_ = os.Remove(storage.DBFile)
})

serviceConfig := config.CredentialServiceConfig{BaseServiceConfig: &config.BaseServiceConfig{Name: "credential"}}
keyStoreService := testKeyStoreService(tt, bolt)
didService := testDIDService(tt, bolt, keyStoreService)
Expand Down Expand Up @@ -194,4 +201,97 @@ func TestCredentialRouter(t *testing.T) {
assert.Error(tt, err)
assert.Contains(tt, err.Error(), fmt.Sprintf("credential not found with id: %s", cred.ID))
})

t.Run("Credential Status List Test", func(tt *testing.T) {
bolt, err := storage.NewBoltDB()
assert.NoError(tt, err)
assert.NotEmpty(tt, bolt)

// remove the db file after the test
tt.Cleanup(func() {
_ = bolt.Close()
_ = os.Remove(storage.DBFile)
})

serviceConfig := config.CredentialServiceConfig{BaseServiceConfig: &config.BaseServiceConfig{Name: "credential"}}
keyStoreService := testKeyStoreService(tt, bolt)
didService := testDIDService(tt, bolt, keyStoreService)
schemaService := testSchemaService(tt, bolt, keyStoreService, didService)
credService, err := credential.NewCredentialService(serviceConfig, bolt, keyStoreService, didService.GetResolver(), schemaService)
assert.NoError(tt, err)
assert.NotEmpty(tt, credService)

// check type and status
assert.Equal(tt, framework.Credential, credService.Type())
assert.Equal(tt, framework.StatusReady, credService.Status().Status)

// create a did
issuerDID, err := didService.CreateDIDByMethod(did.CreateDIDRequest{Method: didsdk.KeyMethod, KeyType: crypto.Ed25519})
assert.NoError(tt, err)
assert.NotEmpty(tt, issuerDID)

// create a schema
emailSchema := map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"email": map[string]interface{}{
"type": "string",
},
},
"required": []interface{}{"email"},
"additionalProperties": false,
}

createdSchema, err := schemaService.CreateSchema(schema.CreateSchemaRequest{Author: "me", Name: "simple schema", Schema: emailSchema})
assert.NoError(tt, err)
assert.NotEmpty(tt, createdSchema)

issuer := issuerDID.DID.ID
subject := "did:test:345"

createdCred, err := credService.CreateCredential(credential.CreateCredentialRequest{
Issuer: issuer,
Subject: subject,
JSONSchema: createdSchema.ID,
Data: map[string]interface{}{
"email": "[email protected]",
},
Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339),
Revocable: true,
})

assert.NoError(tt, err)
assert.NotEmpty(tt, createdCred)
assert.NotEmpty(tt, createdCred.CredentialJWT)

credStatusMap, ok := createdCred.Credential.CredentialStatus.(map[string]interface{})
assert.True(tt, ok)

assert.Contains(tt, credStatusMap["id"], fmt.Sprintf("v1/credentials/%s/status", createdCred.ID))
assert.Contains(tt, credStatusMap["statusListCredential"], "v1/credentials/status")
assert.NotEmpty(tt, credStatusMap["statusListIndex"])

createdCredTwo, err := credService.CreateCredential(credential.CreateCredentialRequest{
Issuer: issuer,
Subject: subject,
JSONSchema: createdSchema.ID,
Data: map[string]interface{}{
"email": "[email protected]",
},
Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339),
Revocable: true,
})

assert.NoError(tt, err)
assert.NotEmpty(tt, createdCredTwo)
assert.NotEmpty(tt, createdCredTwo.CredentialJWT)

credStatusMapTwo, ok := createdCredTwo.Credential.CredentialStatus.(map[string]interface{})
assert.True(tt, ok)

assert.Contains(tt, credStatusMapTwo["id"], fmt.Sprintf("v1/credentials/%s/status", createdCredTwo.ID))
assert.Contains(tt, credStatusMapTwo["statusListCredential"], "v1/credentials/status")
assert.NotEmpty(tt, credStatusMapTwo["statusListIndex"])

})
}
Loading

0 comments on commit 3723d02

Please sign in to comment.