diff --git a/config/compose.toml b/config/compose.toml index a1be04d8f..735a3b5dd 100644 --- a/config/compose.toml +++ b/config/compose.toml @@ -21,6 +21,7 @@ log_level = "info" [services] storage = "bolt" +service_endpoint = "http://localhost:8000" # per-service configuration [services.keystore] diff --git a/config/config.go b/config/config.go index 34a650223..666c824e8 100644 --- a/config/config.go +++ b/config/config.go @@ -18,6 +18,8 @@ const ( ConfigFileName = "config.toml" ServiceName = "ssi-service" ConfigExtension = ".toml" + + DefaultServiceEndpoint = "http://localhost:8000" ) type SSIServiceConfig struct { @@ -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"` @@ -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 { @@ -102,6 +105,7 @@ func (s *SchemaServiceConfig) IsEmpty() bool { type CredentialServiceConfig struct { *BaseServiceConfig + // TODO(gabe) supported key and signature types } @@ -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", @@ -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"}, @@ -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 diff --git a/config/config.toml b/config/config.toml index b176e72e5..e9417d048 100644 --- a/config/config.toml +++ b/config/config.toml @@ -19,6 +19,7 @@ log_level = "debug" [services] storage = "bolt" +service_endpoint = "http://localhost:8000" # per-service configuration [services.keystore] diff --git a/go.mod b/go.mod index 5e7e3f778..df366f2e2 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 1675631a5..9c5e8cdad 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/credential/model.go b/internal/credential/model.go index 406b465a1..a215885c3 100644 --- a/internal/credential/model.go +++ b/internal/credential/model.go @@ -19,6 +19,7 @@ type Container struct { ID string Credential *credential.VerifiableCredential CredentialJWT *keyaccess.JWT + Revoked bool } func (c Container) JWTString() string { diff --git a/pkg/server/router/credential.go b/pkg/server/router/credential.go index 5a3b3e936..d3e1d4dd0 100644 --- a/pkg/server/router/credential.go +++ b/pkg/server/router/credential.go @@ -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. } @@ -60,6 +61,7 @@ func (c CreateCredentialRequest) ToServiceRequest() credential.CreateCredentialR JSONSchema: c.Schema, Data: c.Data, Expiry: c.Expiry, + Revocable: c.Revocable, } } @@ -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"` diff --git a/pkg/server/router/credential_test.go b/pkg/server/router/credential_test.go index 7bc573afb..f7b1474bf 100644 --- a/pkg/server/router/credential_test.go +++ b/pkg/server/router/credential_test.go @@ -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) @@ -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": "Satoshi@Nakamoto.btc", + }, + 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": "Satoshi2@Nakamoto2.btc", + }, + 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"]) + + }) } diff --git a/pkg/server/server.go b/pkg/server/server.go index a53a1aaea..86046cd0c 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -27,6 +27,7 @@ const ( DIDsPrefix = "/dids" SchemasPrefix = "/schemas" CredentialsPrefix = "/credentials" + StatusPrefix = "/status" PresentationsPrefix = "/presentations" DefinitionsPrefix = "/definitions" SubmissionsPrefix = "/submissions" @@ -149,13 +150,20 @@ func (s *SSIServer) CredentialAPI(service svcframework.Service) (err error) { return util.LoggingErrorMsg(err, "could not create credential router") } - handlerPath := V1Prefix + CredentialsPrefix + credentialHandlerPath := V1Prefix + CredentialsPrefix + statusHandlerPath := V1Prefix + CredentialsPrefix + StatusPrefix - s.Handle(http.MethodPut, handlerPath, credRouter.CreateCredential) - s.Handle(http.MethodGet, handlerPath, credRouter.GetCredentials) - s.Handle(http.MethodGet, path.Join(handlerPath, "/:id"), credRouter.GetCredential) - s.Handle(http.MethodPut, path.Join(handlerPath, VerificationPath), credRouter.VerifyCredential) - s.Handle(http.MethodDelete, path.Join(handlerPath, "/:id"), credRouter.DeleteCredential) + // Credentials + s.Handle(http.MethodPut, credentialHandlerPath, credRouter.CreateCredential) + s.Handle(http.MethodGet, credentialHandlerPath, credRouter.GetCredentials) + s.Handle(http.MethodGet, path.Join(credentialHandlerPath, "/:id"), credRouter.GetCredential) + s.Handle(http.MethodPut, path.Join(credentialHandlerPath, VerificationPath), credRouter.VerifyCredential) + s.Handle(http.MethodDelete, path.Join(credentialHandlerPath, "/:id"), credRouter.DeleteCredential) + + // Credential Status + s.Handle(http.MethodGet, path.Join(credentialHandlerPath, "/:id", StatusPrefix), credRouter.GetCredentialStatus) + s.Handle(http.MethodPut, path.Join(credentialHandlerPath, "/:id", StatusPrefix), credRouter.UpdateCredentialStatus) + s.Handle(http.MethodGet, path.Join(statusHandlerPath, "/:id"), credRouter.GetCredentialStatusList) return } diff --git a/pkg/server/server_credential_test.go b/pkg/server/server_credential_test.go index 49ce37d9a..1224ef64c 100644 --- a/pkg/server/server_credential_test.go +++ b/pkg/server/server_credential_test.go @@ -621,4 +621,330 @@ func TestCredentialAPI(t *testing.T) { assert.False(tt, verifyResp.Verified) assert.Contains(tt, verifyResp.Reason, "could not parse credential from JWT") }) + + t.Run("Test Create Revocable Credential", func(tt *testing.T) { + bolt, err := storage.NewBoltDB() + require.NoError(tt, err) + + // remove the db file after the test + tt.Cleanup(func() { + _ = bolt.Close() + _ = os.Remove(storage.DBFile) + }) + + keyStoreService := testKeyStoreService(tt, bolt) + didService := testDIDService(tt, bolt, keyStoreService) + schemaService := testSchemaService(tt, bolt, keyStoreService, didService) + credRouter := testCredentialRouter(tt, bolt, keyStoreService, didService, schemaService) + + issuerDID, err := didService.CreateDIDByMethod(did.CreateDIDRequest{ + Method: didsdk.KeyMethod, + KeyType: crypto.Ed25519, + }) + assert.NoError(tt, err) + assert.NotEmpty(tt, issuerDID) + + issuerDIDTwo, err := didService.CreateDIDByMethod(did.CreateDIDRequest{ + Method: didsdk.KeyMethod, + KeyType: crypto.Ed25519, + }) + assert.NoError(tt, err) + assert.NotEmpty(tt, issuerDIDTwo) + + w := httptest.NewRecorder() + + // good request One + createCredRequest := router.CreateCredentialRequest{ + Issuer: issuerDID.DID.ID, + Subject: "did:abc:456", + Data: map[string]interface{}{ + "firstName": "Jack", + "lastName": "Dorsey", + }, + Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), + } + requestValue := newRequestValue(tt, createCredRequest) + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials", requestValue) + err = credRouter.CreateCredential(newRequestContext(), w, req) + assert.NoError(tt, err) + + var resp router.CreateCredentialResponse + err = json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(tt, err) + + assert.NotEmpty(tt, resp.CredentialJWT) + assert.NoError(tt, err) + assert.Empty(tt, resp.Credential.CredentialStatus) + assert.Equal(tt, resp.Credential.Issuer, issuerDID.DID.ID) + + // good revocable request One + createRevocableCredRequestOne := router.CreateCredentialRequest{ + Issuer: issuerDID.DID.ID, + Subject: "did:abc:456", + Data: map[string]interface{}{ + "firstName": "Jack", + "lastName": "Dorsey", + }, + Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), + Revocable: true, + } + + requestValue = newRequestValue(tt, createRevocableCredRequestOne) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials", requestValue) + err = credRouter.CreateCredential(newRequestContext(), w, req) + assert.NoError(tt, err) + + var revocableRespOne router.CreateCredentialResponse + err = json.NewDecoder(w.Body).Decode(&revocableRespOne) + assert.NoError(tt, err) + + assert.NotEmpty(tt, revocableRespOne.CredentialJWT) + assert.NotEmpty(tt, revocableRespOne.Credential.CredentialStatus) + assert.Equal(tt, revocableRespOne.Credential.Issuer, issuerDID.DID.ID) + + credStatusMap, ok := revocableRespOne.Credential.CredentialStatus.(map[string]interface{}) + assert.True(tt, ok) + + assert.NotEmpty(tt, credStatusMap["statusListIndex"]) + + // good revocable request Two + createRevocableCredRequestTwo := router.CreateCredentialRequest{ + Issuer: issuerDID.DID.ID, + Subject: "did:abc:456", + Data: map[string]interface{}{ + "firstName": "Jack", + "lastName": "Dorsey", + }, + Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), + Revocable: true, + } + + requestValue = newRequestValue(tt, createRevocableCredRequestTwo) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials", requestValue) + err = credRouter.CreateCredential(newRequestContext(), w, req) + assert.NoError(tt, err) + + var revocableRespTwo router.CreateCredentialResponse + err = json.NewDecoder(w.Body).Decode(&revocableRespTwo) + assert.NoError(tt, err) + + assert.NotEmpty(tt, revocableRespTwo.CredentialJWT) + assert.NotEmpty(tt, revocableRespTwo.Credential.CredentialStatus) + assert.Equal(tt, revocableRespTwo.Credential.Issuer, issuerDID.DID.ID) + + credStatusMap, ok = revocableRespTwo.Credential.CredentialStatus.(map[string]interface{}) + assert.True(tt, ok) + + assert.NotEmpty(tt, credStatusMap["statusListIndex"]) + + // good revocable request Three + createRevocableCredRequestThree := router.CreateCredentialRequest{ + Issuer: issuerDID.DID.ID, + Subject: "did:abc:456", + Data: map[string]interface{}{ + "firstName": "Jack", + "lastName": "Dorsey", + }, + Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), + Revocable: true, + } + + requestValue = newRequestValue(tt, createRevocableCredRequestThree) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials", requestValue) + err = credRouter.CreateCredential(newRequestContext(), w, req) + assert.NoError(tt, err) + + var revocableRespThree router.CreateCredentialResponse + err = json.NewDecoder(w.Body).Decode(&revocableRespThree) + assert.NoError(tt, err) + + assert.NotEmpty(tt, revocableRespThree.CredentialJWT) + assert.NotEmpty(tt, revocableRespThree.Credential.CredentialStatus) + assert.Equal(tt, revocableRespThree.Credential.Issuer, issuerDID.DID.ID) + + credStatusMap, ok = revocableRespThree.Credential.CredentialStatus.(map[string]interface{}) + assert.True(tt, ok) + + assert.NotEmpty(tt, credStatusMap["statusListIndex"]) + + // good revocable request Four (different issuer / schema) + createRevocableCredRequestFour := router.CreateCredentialRequest{ + Issuer: issuerDIDTwo.DID.ID, + Subject: "did:abc:456", + Data: map[string]interface{}{ + "firstName": "Jack", + "lastName": "Dorsey", + }, + Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), + Revocable: true, + } + + requestValue = newRequestValue(tt, createRevocableCredRequestFour) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials", requestValue) + err = credRouter.CreateCredential(newRequestContext(), w, req) + assert.NoError(tt, err) + + var revocableRespFour router.CreateCredentialResponse + err = json.NewDecoder(w.Body).Decode(&revocableRespFour) + assert.NoError(tt, err) + + assert.NotEmpty(tt, revocableRespFour.CredentialJWT) + assert.NotEmpty(tt, revocableRespFour.Credential.CredentialStatus) + assert.Equal(tt, revocableRespFour.Credential.Issuer, issuerDIDTwo.DID.ID) + + credStatusMap, ok = revocableRespFour.Credential.CredentialStatus.(map[string]interface{}) + assert.True(tt, ok) + + assert.NotEmpty(tt, credStatusMap["statusListIndex"]) + }) + + t.Run("Test Get Revoked Status Of Credential", func(tt *testing.T) { + bolt, err := storage.NewBoltDB() + require.NoError(tt, err) + + // remove the db file after the test + tt.Cleanup(func() { + _ = bolt.Close() + _ = os.Remove(storage.DBFile) + }) + + keyStoreService := testKeyStoreService(tt, bolt) + didService := testDIDService(tt, bolt, keyStoreService) + schemaService := testSchemaService(tt, bolt, keyStoreService, didService) + credRouter := testCredentialRouter(tt, bolt, keyStoreService, didService, schemaService) + + issuerDID, err := didService.CreateDIDByMethod(did.CreateDIDRequest{ + Method: didsdk.KeyMethod, + KeyType: crypto.Ed25519, + }) + assert.NoError(tt, err) + assert.NotEmpty(tt, issuerDID) + + w := httptest.NewRecorder() + + // good request number one + createCredRequest := router.CreateCredentialRequest{ + Issuer: issuerDID.DID.ID, + Subject: "did:abc:456", + Data: map[string]interface{}{ + "firstName": "Jack", + "lastName": "Dorsey", + }, + Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), + Revocable: true, + } + + requestValue := newRequestValue(tt, createCredRequest) + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials", requestValue) + err = credRouter.CreateCredential(newRequestContext(), w, req) + assert.NoError(tt, err) + + var resp router.CreateCredentialResponse + err = json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(tt, err) + + assert.NotEmpty(tt, resp.CredentialJWT) + assert.NotEmpty(tt, resp.Credential.CredentialStatus) + assert.Equal(tt, resp.Credential.Issuer, issuerDID.DID.ID) + + credStatusMap, ok := resp.Credential.CredentialStatus.(map[string]interface{}) + assert.True(tt, ok) + + assert.NotEmpty(tt, credStatusMap["statusListIndex"]) + + req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/credentials/%s/status", resp.Credential.ID), nil) + err = credRouter.GetCredentialStatus(newRequestContextWithParams(map[string]string{"id": resp.Credential.ID}), w, req) + assert.NoError(tt, err) + + var credStatusResponse = router.GetCredentialStatusResponse{} + err = json.NewDecoder(w.Body).Decode(&credStatusResponse) + assert.NoError(tt, err) + assert.Equal(tt, false, credStatusResponse.Revoked) + + // good request number one + updateCredStatusRequest := router.UpdateCredentialStatusRequest{Revoked: true} + + requestValue = newRequestValue(tt, updateCredStatusRequest) + req = httptest.NewRequest(http.MethodPut, fmt.Sprintf("https://ssi-service.com/v1/credentials/%s/status", resp.Credential.ID), requestValue) + err = credRouter.UpdateCredentialStatus(newRequestContextWithParams(map[string]string{"id": resp.Credential.ID}), w, req) + assert.NoError(tt, err) + + var credStatusUpdateResponse = router.UpdateCredentialStatusResponse{} + err = json.NewDecoder(w.Body).Decode(&credStatusUpdateResponse) + assert.NoError(tt, err) + assert.Equal(tt, true, credStatusUpdateResponse.Revoked) + + }) + + t.Run("Test Get Status List Credential", func(tt *testing.T) { + bolt, err := storage.NewBoltDB() + require.NoError(tt, err) + + // remove the db file after the test + tt.Cleanup(func() { + _ = bolt.Close() + _ = os.Remove(storage.DBFile) + }) + + keyStoreService := testKeyStoreService(tt, bolt) + didService := testDIDService(tt, bolt, keyStoreService) + schemaService := testSchemaService(tt, bolt, keyStoreService, didService) + credRouter := testCredentialRouter(tt, bolt, keyStoreService, didService, schemaService) + + issuerDID, err := didService.CreateDIDByMethod(did.CreateDIDRequest{ + Method: didsdk.KeyMethod, + KeyType: crypto.Ed25519, + }) + assert.NoError(tt, err) + assert.NotEmpty(tt, issuerDID) + + w := httptest.NewRecorder() + + // good request number one + createCredRequest := router.CreateCredentialRequest{ + Issuer: issuerDID.DID.ID, + Subject: "did:abc:456", + Data: map[string]interface{}{ + "firstName": "Jack", + "lastName": "Dorsey", + }, + Expiry: time.Now().Add(24 * time.Hour).Format(time.RFC3339), + Revocable: true, + } + + requestValue := newRequestValue(tt, createCredRequest) + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials", requestValue) + err = credRouter.CreateCredential(newRequestContext(), w, req) + assert.NoError(tt, err) + + var resp router.CreateCredentialResponse + err = json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(tt, err) + + assert.NotEmpty(tt, resp.CredentialJWT) + assert.NotEmpty(tt, resp.Credential.CredentialStatus) + assert.Equal(tt, resp.Credential.Issuer, issuerDID.DID.ID) + + credStatusMap, ok := resp.Credential.CredentialStatus.(map[string]interface{}) + assert.True(tt, ok) + + assert.NotEmpty(tt, credStatusMap["statusListIndex"]) + + credStatusListID := (credStatusMap["statusListCredential"]).(string) + + assert.NotEmpty(tt, credStatusListID) + fmt.Println(credStatusListID) + + req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://localhost:8080/v1/credentials/status/%s", credStatusListID), nil) + err = credRouter.GetCredentialStatusList(newRequestContextWithParams(map[string]string{"id": credStatusListID}), w, req) + assert.NoError(tt, err) + + var credListResp router.GetCredentialStatusListResponse + err = json.NewDecoder(w.Body).Decode(&credListResp) + assert.NoError(tt, err) + + assert.NotEmpty(tt, credListResp.CredentialJWT) + assert.Empty(tt, credListResp.Credential.CredentialStatus) + assert.Equal(tt, credListResp.Credential.ID, credStatusListID) + }) } diff --git a/pkg/service/credential/model.go b/pkg/service/credential/model.go index 68f9c106e..b3a02084b 100644 --- a/pkg/service/credential/model.go +++ b/pkg/service/credential/model.go @@ -17,6 +17,7 @@ type CreateCredentialRequest struct { JSONSchema string `json:"jsonSchema,omitempty"` Data map[string]interface{} `json:"data,omitempty"` Expiry string `json:"expiry,omitempty"` + Revocable bool `json:"revocable,omitempty"` // TODO(gabe) support more capabilities like signature type, format, status, and more. } @@ -53,3 +54,28 @@ type GetCredentialsResponse struct { type DeleteCredentialRequest struct { ID string `json:"id" validate:"required"` } + +type GetCredentialStatusRequest struct { + ID string `json:"id" validate:"required"` +} + +type GetCredentialStatusResponse struct { + Revoked bool `json:"revoked" validate:"required"` +} + +type UpdateCredentialStatusRequest struct { + ID string `json:"id" validate:"required"` + Revoked bool `json:"revoked" validate:"required"` +} + +type UpdateCredentialStatusResponse struct { + Revoked bool `json:"revoked" validate:"required"` +} + +type GetCredentialStatusListRequest struct { + ID string `json:"id" validate:"required"` +} + +type GetCredentialStatusListResponse struct { + credential.Container `json:"credential,omitempty"` +} diff --git a/pkg/service/credential/service.go b/pkg/service/credential/service.go index 88f7d085e..3bd2bf6ba 100644 --- a/pkg/service/credential/service.go +++ b/pkg/service/credential/service.go @@ -2,6 +2,9 @@ package credential import ( "fmt" + statussdk "github.com/TBD54566975/ssi-sdk/credential/status" + "github.com/google/uuid" + "strconv" "time" "github.com/TBD54566975/ssi-sdk/credential" @@ -146,6 +149,34 @@ func (s Service) CreateCredential(request CreateCredentialRequest) (*CreateCrede return nil, util.LoggingErrorMsg(err, errMsg) } + if request.Revocable == true { + credID := builder.ID + issuerID := request.Issuer + schemaID := request.JSONSchema + + statusListCredential, err := getStatusListCredential(s, issuerID, schemaID) + if err != nil { + return nil, util.LoggingErrorMsgf(err, "problem with getting status list credential") + } + + statusListIndex, err := s.storage.GetNextStatusListRandomIndex() + if err != nil { + return nil, util.LoggingErrorMsg(err, "problem with getting status list index") + } + + status := statussdk.StatusList2021Entry{ + ID: fmt.Sprintf(`%s/v1/credentials/%s/status`, s.config.ServiceEndpoint, credID), + Type: statussdk.StatusList2021EntryType, + StatusPurpose: statussdk.StatusRevocation, + StatusListIndex: strconv.Itoa(statusListIndex), + StatusListCredential: statusListCredential.ID, + } + + if err := builder.SetCredentialStatus(status); err != nil { + return nil, util.LoggingErrorMsg(err, "could not set credential status") + } + } + cred, err := builder.Build() if err != nil { return nil, util.LoggingErrorMsg(err, "could not build credential") @@ -170,10 +201,13 @@ func (s Service) CreateCredential(request CreateCredentialRequest) (*CreateCrede ID: cred.ID, Credential: cred, CredentialJWT: credJWT, + Revoked: false, } + storageRequest := credstorage.StoreCredentialRequest{ Container: container, } + if err = s.storage.StoreCredential(storageRequest); err != nil { return nil, util.LoggingErrorMsg(err, "could not store credential") } @@ -183,6 +217,56 @@ func (s Service) CreateCredential(request CreateCredentialRequest) (*CreateCrede return &response, nil } +func getStatusListCredential(s Service, issuerID string, schemaID string) (*credential.VerifiableCredential, error) { + storedStatusListCreds, err := s.storage.GetStatusListCredentialsByIssuerAndSchema(issuerID, schemaID) + if err != nil { + return nil, util.LoggingNewErrorf("problem with getting status list credential for issuer: %s schema: %s", issuerID, schemaID) + } + + // This should never happen, there should always be only 1 status list credential per pair + if len(storedStatusListCreds) > 1 { + return nil, util.LoggingNewErrorf("only one status list credential per pair allowed. issuer: %s schema: %s", issuerID, schemaID) + } + + var statusListCredential *credential.VerifiableCredential + + // First time that this pair has a revocation or suspension credential issued + if len(storedStatusListCreds) == 0 { + statusListID := fmt.Sprintf("%s/v1/credentials/status/%s", s.config.ServiceEndpoint, uuid.New().String()) + generatedStatusListCredential, err := statussdk.GenerateStatusList2021Credential(statusListID, issuerID, statussdk.StatusRevocation, []credential.VerifiableCredential{}) + if err != nil { + return nil, util.LoggingErrorMsg(err, "could not generate status list") + } + + statusListCredJWT, err := s.signCredentialJWT(issuerID, *generatedStatusListCredential) + if err != nil { + return nil, util.LoggingErrorMsg(err, "could not sign status list credential") + } + + // store the credential + statusListContainer := credint.Container{ + ID: generatedStatusListCredential.ID, + Credential: generatedStatusListCredential, + CredentialJWT: statusListCredJWT, + } + + storageRequest := credstorage.StoreCredentialRequest{ + Container: statusListContainer, + } + + if err = s.storage.StoreStatusListCredential(storageRequest); err != nil { + return nil, util.LoggingErrorMsg(err, "could not store credential") + } + + statusListCredential = generatedStatusListCredential + + } else { + statusListCredential = storedStatusListCreds[0].Credential + } + + return statusListCredential, nil +} + // signCredentialJWT signs a credential and returns it as a vc-jwt func (s Service) signCredentialJWT(issuer string, cred credential.VerifiableCredential) (*keyaccess.JWT, error) { gotKey, err := s.keyStore.GetKey(keystore.GetKeyRequest{ID: issuer}) @@ -338,6 +422,146 @@ func (s Service) GetCredentialsBySchema(request GetCredentialBySchemaRequest) (* return &response, nil } +func (s Service) GetCredentialStatus(request GetCredentialStatusRequest) (*GetCredentialStatusResponse, error) { + logrus.Debugf("getting credential status: %s", request.ID) + + gotCred, err := s.storage.GetCredential(request.ID) + if err != nil { + return nil, util.LoggingErrorMsgf(err, "could not get credential: %s", request.ID) + } + if !gotCred.IsValid() { + return nil, util.LoggingNewErrorf("credential returned is not valid: %s", request.ID) + } + response := GetCredentialStatusResponse{ + Revoked: gotCred.Revoked, + } + return &response, nil +} + +func (s Service) GetCredentialStatusList(request GetCredentialStatusListRequest) (*GetCredentialStatusListResponse, error) { + logrus.Debugf("getting credential status list: %s", request.ID) + + gotCred, err := s.storage.GetStatusListCredential(request.ID) + if err != nil { + return nil, util.LoggingErrorMsgf(err, "could not get credential: %s", request.ID) + } + if !gotCred.IsValid() { + return nil, util.LoggingNewErrorf("credential returned is not valid: %s", request.ID) + } + response := GetCredentialStatusListResponse{ + credint.Container{ + ID: gotCred.CredentialID, + Credential: gotCred.Credential, + CredentialJWT: gotCred.CredentialJWT, + }, + } + return &response, nil +} + +func (s Service) UpdateCredentialStatus(request UpdateCredentialStatusRequest) (*UpdateCredentialStatusResponse, error) { + logrus.Debugf("updating credential status: %s to Revoked: %v", request.ID, request.Revoked) + + gotCred, err := s.storage.GetCredential(request.ID) + if err != nil { + return nil, util.LoggingErrorMsgf(err, "could not get credential: %s", request.ID) + } + if !gotCred.IsValid() { + return nil, util.LoggingNewErrorf("credential returned is not valid: %s", request.ID) + } + + // if the request is the same as what the current credential is there is no action + if gotCred.Revoked == request.Revoked { + response := UpdateCredentialStatusResponse{Revoked: gotCred.Revoked} + return &response, nil + } + + container, err := updateCredentialStatus(s, gotCred, request) + if err != nil { + return nil, util.LoggingNewError("problem updating credential") + } + + response := UpdateCredentialStatusResponse{Revoked: container.Revoked} + return &response, nil +} + +func updateCredentialStatus(s Service, gotCred *credstorage.StoredCredential, request UpdateCredentialStatusRequest) (*credint.Container, error) { + // store the credential with updated status + container := credint.Container{ + ID: gotCred.ID, + Credential: gotCred.Credential, + CredentialJWT: gotCred.CredentialJWT, + Revoked: request.Revoked, + } + + storageRequest := credstorage.StoreCredentialRequest{ + Container: container, + } + + if err := s.storage.StoreCredential(storageRequest); err != nil { + return nil, util.LoggingErrorMsg(err, "could not store credential") + } + + storedStatusListCreds, err := s.storage.GetStatusListCredentialsByIssuerAndSchema(gotCred.Issuer, gotCred.Schema) + if err != nil { + return nil, util.LoggingNewErrorf("problem with getting status list credential for issuer: %s schema: %s", gotCred.Issuer, gotCred.Schema) + } + + creds, err := s.storage.GetCredentialsByIssuerAndSchema(gotCred.Issuer, gotCred.Schema) + if err != nil { + return nil, util.LoggingNewErrorf("problem with getting status list credential for issuer: %s schema: %s", gotCred.Issuer, gotCred.Schema) + } + + var revokedStatusCreds []credential.VerifiableCredential + for _, cred := range creds { + if cred.Credential.CredentialStatus != nil && cred.Revoked { + revokedStatusCreds = append(revokedStatusCreds, *cred.Credential) + } + } + + generatedStatusListCredential, err := statussdk.GenerateStatusList2021Credential(storedStatusListCreds[0].ID, gotCred.Issuer, statussdk.StatusRevocation, revokedStatusCreds) + if err != nil { + return nil, util.LoggingErrorMsg(err, "could not generate status list") + } + + statusListCredJWT, err := s.signCredentialJWT(gotCred.Issuer, *generatedStatusListCredential) + if err != nil { + return nil, util.LoggingErrorMsg(err, "could not sign status list credential") + } + + // store the status list credential + statusListContainer := credint.Container{ + ID: generatedStatusListCredential.ID, + Credential: generatedStatusListCredential, + CredentialJWT: statusListCredJWT, + } + + storageRequest = credstorage.StoreCredentialRequest{ + Container: statusListContainer, + } + + if err = s.storage.StoreStatusListCredential(storageRequest); err != nil { + return nil, util.LoggingErrorMsg(err, "could not store credential status list") + } + + return &container, nil +} + +func (s Service) GetCredentialsByIssuerAndSchemaWithStatus(issuer string, schema string) ([]credential.VerifiableCredential, error) { + gotCreds, err := s.storage.GetCredentialsByIssuerAndSchema(issuer, schema) + if err != nil { + return nil, util.LoggingErrorMsgf(err, "could not get credential(s) for issuer: %s", issuer) + } + + var creds []credential.VerifiableCredential + for _, cred := range gotCreds { + if cred.Credential.CredentialStatus != nil { + creds = append(creds, *cred.Credential) + } + } + + return creds, nil +} + func (s Service) DeleteCredential(request DeleteCredentialRequest) error { logrus.Debugf("deleting credential: %s", request.ID) diff --git a/pkg/service/credential/storage/bolt.go b/pkg/service/credential/storage/bolt.go index 8eddc8d5f..6b0ef9f9c 100644 --- a/pkg/service/credential/storage/bolt.go +++ b/pkg/service/credential/storage/bolt.go @@ -2,19 +2,29 @@ package storage import ( "fmt" - "strings" - "github.com/TBD54566975/ssi-sdk/credential/signing" "github.com/goccy/go-json" "github.com/pkg/errors" "github.com/sirupsen/logrus" + "math/rand" + "strings" "github.com/tbd54566975/ssi-service/internal/util" "github.com/tbd54566975/ssi-service/pkg/storage" ) const ( - namespace = "credential" + credentialNamespace = "credential" + statusListCredentialNamespace = "status-list-credential" + statusListIndexNamespace = "status-list-index" + + fakeKey = "fake-key" + statusListIndexesKey = "status-list-indexes" + currentListIndexKey = "current-list-index" + + // A a minimum revocation bitString length of 131,072, or 16KB uncompressed + bitStringLength = 8 * 1024 * 16 + credentialNotFoundErrMsg = "credential not found" ) @@ -22,14 +32,103 @@ type BoltCredentialStorage struct { db *storage.BoltDB } +type StatusListIndex struct { + Index int `json:"index"` +} + func NewBoltCredentialStorage(db *storage.BoltDB) (*BoltCredentialStorage, error) { if db == nil { return nil, errors.New("bolt db reference is nil") } + + // TODO: (Neal) there is a current bug with our Bolt implementation where if we do a GET without anything in the db it will throw an error + // Doing initial writes and then deleting will "warm up" our database and when we do a GET after that it will not crash and return empty list + // https://github.com/TBD54566975/ssi-service/issues/176 + if err := db.Write(credentialNamespace, fakeKey, nil); err != nil { + return nil, util.LoggingErrorMsg(err, "problem writing status initial write to db") + + } + if err := db.Delete(credentialNamespace, fakeKey); err != nil { + return nil, util.LoggingErrorMsg(err, "problem with initial delete to db") + } + + if err := db.Write(statusListCredentialNamespace, fakeKey, nil); err != nil { + return nil, util.LoggingErrorMsg(err, "problem writing status initial write to db") + } + + if err := db.Delete(statusListCredentialNamespace, fakeKey); err != nil { + return nil, util.LoggingErrorMsg(err, "problem with initial delete to db") + } + + randUniqueList := randomUniqueNum(bitStringLength) + uniqueNumBytes, err := json.Marshal(randUniqueList) + if err != nil { + return nil, util.LoggingErrorMsg(err, "could not marshal random unique numbers") + } + + if err := db.Write(statusListIndexNamespace, statusListIndexesKey, uniqueNumBytes); err != nil { + return nil, util.LoggingErrorMsg(err, "problem writing status list indexes to db") + } + + statusListIndexBytes, err := json.Marshal(StatusListIndex{Index: 0}) + if err != nil { + return nil, util.LoggingErrorMsg(err, "could not marshal status list index bytes") + } + + if err := db.Write(statusListIndexNamespace, currentListIndexKey, statusListIndexBytes); err != nil { + return nil, util.LoggingErrorMsg(err, "problem writing current list index to db") + } + return &BoltCredentialStorage{db: db}, nil } +func (b BoltCredentialStorage) GetNextStatusListRandomIndex() (int, error) { + + gotUniqueNumBytes, err := b.db.Read(statusListIndexNamespace, statusListIndexesKey) + if err != nil { + return -1, util.LoggingErrorMsgf(err, "reading status list") + } + + if len(gotUniqueNumBytes) == 0 { + return -1, util.LoggingNewErrorf("could not get unique numbers from db") + } + + var uniqueNums []int + if err = json.Unmarshal(gotUniqueNumBytes, &uniqueNums); err != nil { + return -1, util.LoggingErrorMsgf(err, "could not unmarshal unique numbers") + } + + gotCurrentListIndexBytes, err := b.db.Read(statusListIndexNamespace, currentListIndexKey) + if err != nil { + return -1, util.LoggingErrorMsgf(err, "could not get list index") + } + + var statusListIndex StatusListIndex + if err = json.Unmarshal(gotCurrentListIndexBytes, &statusListIndex); err != nil { + return -1, util.LoggingErrorMsgf(err, "could not unmarshal unique numbers") + } + + statusListIndexBytes, err := json.Marshal(StatusListIndex{Index: statusListIndex.Index + 1}) + if err != nil { + return -1, util.LoggingErrorMsg(err, "could not marshal status list index bytes") + } + + if err := b.db.Write(statusListIndexNamespace, currentListIndexKey, statusListIndexBytes); err != nil { + return -1, util.LoggingErrorMsg(err, "problem writing current list index to db") + } + + return uniqueNums[statusListIndex.Index], nil +} + func (b BoltCredentialStorage) StoreCredential(request StoreCredentialRequest) error { + return b.storeCredential(request, credentialNamespace) +} + +func (b BoltCredentialStorage) StoreStatusListCredential(request StoreCredentialRequest) error { + return b.storeCredential(request, statusListCredentialNamespace) +} + +func (b BoltCredentialStorage) storeCredential(request StoreCredentialRequest, namespace string) error { if !request.IsValid() { return util.LoggingNewError("store request request is not valid") } @@ -81,10 +180,19 @@ func buildStoredCredential(request StoreCredentialRequest) (*StoredCredential, e Subject: subject, Schema: schema, IssuanceDate: cred.IssuanceDate, + Revoked: request.Revoked, }, nil } func (b BoltCredentialStorage) GetCredential(id string) (*StoredCredential, error) { + return b.getCredential(id, credentialNamespace) +} + +func (b BoltCredentialStorage) GetStatusListCredential(id string) (*StoredCredential, error) { + return b.getCredential(id, statusListCredentialNamespace) +} + +func (b BoltCredentialStorage) getCredential(id string, namespace string) (*StoredCredential, error) { prefixValues, err := b.db.ReadPrefix(namespace, id) if err != nil { return nil, util.LoggingErrorMsgf(err, "could not get credential from storage: %s", id) @@ -115,10 +223,10 @@ func (b BoltCredentialStorage) GetCredential(id string) (*StoredCredential, erro // so this is not much of a concern. // GetCredentialsByIssuer gets all credentials stored with a prefix key containing the issuer value -// The method is greedy, meaning if multiple values are found...and some fail during processing, we will +// The method is greedy, meaning if multiple values are found and some fail during processing, we will // return only the successful values and log an error for the failures. func (b BoltCredentialStorage) GetCredentialsByIssuer(issuer string) ([]StoredCredential, error) { - keys, err := b.db.ReadAllKeys(namespace) + keys, err := b.db.ReadAllKeys(credentialNamespace) if err != nil { return nil, util.LoggingErrorMsgf(err, "could not read credential storage while searching for creds for issuer: %s", issuer) } @@ -137,7 +245,7 @@ func (b BoltCredentialStorage) GetCredentialsByIssuer(issuer string) ([]StoredCr // now get each credential by key var storedCreds []StoredCredential for _, key := range issuerKeys { - credBytes, err := b.db.Read(namespace, key) + credBytes, err := b.db.Read(credentialNamespace, key) if err != nil { logrus.WithError(err).Errorf("could not read credential with key: %s", key) } else { @@ -160,7 +268,7 @@ func (b BoltCredentialStorage) GetCredentialsByIssuer(issuer string) ([]StoredCr // The method is greedy, meaning if multiple values are found...and some fail during processing, we will // return only the successful values and log an error for the failures. func (b BoltCredentialStorage) GetCredentialsBySubject(subject string) ([]StoredCredential, error) { - keys, err := b.db.ReadAllKeys(namespace) + keys, err := b.db.ReadAllKeys(credentialNamespace) if err != nil { return nil, util.LoggingErrorMsgf(err, "could not read credential storage while searching for creds for subject: %s", subject) } @@ -180,7 +288,7 @@ func (b BoltCredentialStorage) GetCredentialsBySubject(subject string) ([]Stored // now get each credential by key var storedCreds []StoredCredential for _, key := range subjectKeys { - credBytes, err := b.db.Read(namespace, key) + credBytes, err := b.db.Read(credentialNamespace, key) if err != nil { logrus.WithError(err).Errorf("could not read credential with key: %s", key) } else { @@ -203,7 +311,7 @@ func (b BoltCredentialStorage) GetCredentialsBySubject(subject string) ([]Stored // The method is greedy, meaning if multiple values are found...and some fail during processing, we will // return only the successful values and log an error for the failures. func (b BoltCredentialStorage) GetCredentialsBySchema(schema string) ([]StoredCredential, error) { - keys, err := b.db.ReadAllKeys(namespace) + keys, err := b.db.ReadAllKeys(credentialNamespace) if err != nil { return nil, util.LoggingErrorMsgf(err, "could not read credential storage while searching for creds for schema: %s", schema) } @@ -224,7 +332,7 @@ func (b BoltCredentialStorage) GetCredentialsBySchema(schema string) ([]StoredCr // now get each credential by key var storedCreds []StoredCredential for _, key := range schemaKeys { - credBytes, err := b.db.Read(namespace, key) + credBytes, err := b.db.Read(credentialNamespace, key) if err != nil { logrus.WithError(err).Errorf("could not read credential with key: %s", key) } else { @@ -243,7 +351,67 @@ func (b BoltCredentialStorage) GetCredentialsBySchema(schema string) ([]StoredCr return storedCreds, nil } +// GetCredentialsByIssuerAndSchema gets all credentials stored with a prefix key containing the issuer value +// The method is greedy, meaning if multiple values are found...and some fail during processing, we will +// return only the successful values and log an error for the failures. +func (b BoltCredentialStorage) GetCredentialsByIssuerAndSchema(issuer string, schema string) ([]StoredCredential, error) { + return b.getCredentialsByIssuerAndSchema(issuer, schema, credentialNamespace) +} + +func (b BoltCredentialStorage) GetStatusListCredentialsByIssuerAndSchema(issuer string, schema string) ([]StoredCredential, error) { + return b.getCredentialsByIssuerAndSchema(issuer, schema, statusListCredentialNamespace) +} + +func (b BoltCredentialStorage) getCredentialsByIssuerAndSchema(issuer string, schema string, namespace string) ([]StoredCredential, error) { + keys, err := b.db.ReadAllKeys(namespace) + if err != nil { + return nil, util.LoggingErrorMsgf(err, "could not read credential storage while searching for creds for issuer: %s", issuer) + } + + query := "sc:" + schema + var issuerSchemaKeys []string + for _, k := range keys { + if strings.Contains(k, issuer) && strings.HasSuffix(k, query) { + issuerSchemaKeys = append(issuerSchemaKeys, k) + } + } + + if len(issuerSchemaKeys) == 0 { + logrus.Warnf("no credentials found for issuer: %s and schema %s", util.SanitizeLog(issuer), util.SanitizeLog(schema)) + return nil, nil + } + + // now get each credential by key + var storedCreds []StoredCredential + for _, key := range issuerSchemaKeys { + credBytes, err := b.db.Read(namespace, key) + if err != nil { + logrus.WithError(err).Errorf("could not read credential with key: %s", key) + } else { + var cred StoredCredential + if err = json.Unmarshal(credBytes, &cred); err != nil { + logrus.WithError(err).Errorf("could not unmarshal credential with key: %s", key) + } + storedCreds = append(storedCreds, cred) + } + } + + if len(storedCreds) == 0 { + logrus.Warnf("no credentials able to be retrieved for issuer: %s", issuerSchemaKeys) + } + + return storedCreds, nil +} + func (b BoltCredentialStorage) DeleteCredential(id string) error { + return b.deleteCredential(id, credentialNamespace) +} + +func (b BoltCredentialStorage) DeleteStatusListCredential(id string) error { + return b.deleteCredential(id, statusListCredentialNamespace) +} + +func (b BoltCredentialStorage) deleteCredential(id string, namespace string) error { credDoesNotExistMsg := fmt.Sprintf("credential does not exist, cannot delete: %s", id) // first get the credential to regenerate the prefix key @@ -276,3 +444,17 @@ func (b BoltCredentialStorage) DeleteCredential(id string) error { func createPrefixKey(id, issuer, subject, schema string) string { return strings.Join([]string{id, "is:" + issuer, "su:" + subject, "sc:" + schema}, "-") } + +func randomUniqueNum(count int) []int { + randomNumbers := make([]int, 0, count) + + for i := 1; i <= count; i++ { + randomNumbers = append(randomNumbers, i) + } + + rand.Shuffle(len(randomNumbers), func(i, j int) { + randomNumbers[i], randomNumbers[j] = randomNumbers[j], randomNumbers[i] + }) + + return randomNumbers +} diff --git a/pkg/service/credential/storage/storage.go b/pkg/service/credential/storage/storage.go index cbeb36ae5..4b4b30bf5 100644 --- a/pkg/service/credential/storage/storage.go +++ b/pkg/service/credential/storage/storage.go @@ -27,6 +27,7 @@ type StoredCredential struct { Subject string `json:"subject"` Schema string `json:"schema"` IssuanceDate string `json:"issuanceDate"` + Revoked bool `json:"revoked"` } func (sc StoredCredential) IsValid() bool { @@ -47,7 +48,15 @@ type Storage interface { GetCredentialsByIssuer(issuer string) ([]StoredCredential, error) GetCredentialsBySubject(subject string) ([]StoredCredential, error) GetCredentialsBySchema(schema string) ([]StoredCredential, error) + GetCredentialsByIssuerAndSchema(issuer, schema string) ([]StoredCredential, error) DeleteCredential(id string) error + + StoreStatusListCredential(request StoreCredentialRequest) error + GetStatusListCredential(id string) (*StoredCredential, error) + GetStatusListCredentialsByIssuerAndSchema(issuer, schema string) ([]StoredCredential, error) + DeleteStatusListCredential(id string) error + + GetNextStatusListRandomIndex() (int, error) } func NewCredentialStorage(s storage.ServiceStorage) (Storage, error) {