From f4983b2145ec6ea869d9cae6b9c65761e7c537db Mon Sep 17 00:00:00 2001 From: Gabe Date: Tue, 19 Jul 2022 14:43:51 -0700 Subject: [PATCH] Add a KeyStore Service (#62) * temp * service boilerplate * add keystore * simple argon2 and xchacha20poly1305 impl * add service key generation * temp...need to generically marshal keys * keystore encryption and decryption tests * key storage tests * add godoc * update swagger * keystore router test * close bolt after each test --- config/config.go | 26 +- docs/swagger.yaml | 119 ++++++- go.mod | 4 +- go.sum | 10 +- internal/util/crypto.go | 96 ++++++ internal/util/crypto_test.go | 48 +++ internal/util/util.go | 7 +- magefile.go | 2 +- pkg/server/router/credential.go | 267 +++++++-------- pkg/server/router/did_test.go | 148 ++++----- pkg/server/router/keystore.go | 127 +++++++ pkg/server/router/keystore_test.go | 97 ++++++ pkg/server/router/router_test.go | 29 ++ pkg/server/server.go | 17 + pkg/server/server_test.go | 229 ++++++++++--- pkg/service/credential/credential.go | 303 ++++++++--------- pkg/service/credential/storage/bolt.go | 383 +++++++++++----------- pkg/service/credential/storage/storage.go | 26 +- pkg/service/did/key.go | 9 + pkg/service/did/storage/storage.go | 28 +- pkg/service/framework/framework.go | 27 +- pkg/service/keystore/keystore.go | 146 +++++++++ pkg/service/keystore/keystore_test.go | 86 +++++ pkg/service/keystore/model.go | 23 ++ pkg/service/keystore/storage/bolt.go | 96 ++++++ pkg/service/keystore/storage/storage.go | 59 ++++ pkg/service/schema/storage/storage.go | 28 +- pkg/service/service.go | 3 +- pkg/storage/bolt.go | 9 +- pkg/storage/storage.go | 1 + 30 files changed, 1793 insertions(+), 660 deletions(-) create mode 100644 internal/util/crypto.go create mode 100644 internal/util/crypto_test.go create mode 100644 pkg/server/router/keystore.go create mode 100644 pkg/server/router/keystore_test.go create mode 100644 pkg/server/router/router_test.go create mode 100644 pkg/service/keystore/keystore.go create mode 100644 pkg/service/keystore/keystore_test.go create mode 100644 pkg/service/keystore/model.go create mode 100644 pkg/service/keystore/storage/bolt.go create mode 100644 pkg/service/keystore/storage/storage.go diff --git a/config/config.go b/config/config.go index 67302089d..18f7e56cb 100644 --- a/config/config.go +++ b/config/config.go @@ -2,14 +2,15 @@ package config import ( "fmt" - "github.com/BurntSushi/toml" - "github.com/ardanlabs/conf" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" "os" "path/filepath" "reflect" "time" + + "github.com/BurntSushi/toml" + "github.com/ardanlabs/conf" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" ) const ( @@ -32,7 +33,7 @@ type ServerConfig struct { ReadTimeout time.Duration `toml:"read_timeout" conf:"default:5s"` WriteTimeout time.Duration `toml:"write_timeout" conf:"default:5s"` ShutdownTimeout time.Duration `toml:"shutdown_timeout" conf:"default:5s"` - LogLocation string `toml:"log_location"` + LogLocation string `toml:"log_location" conf:"default:log"` LogLevel string `toml:"log_level" conf:"default:debug"` } @@ -48,6 +49,7 @@ type ServicesConfig struct { DIDConfig DIDServiceConfig `toml:"did,omitempty"` SchemaConfig SchemaServiceConfig `toml:"schema,omitempty"` CredentialConfig CredentialServiceConfig `toml:"credential,omitempty"` + KeyStoreConfig KeyStoreServiceConfig `toml:"keystore,omitempty"` } // BaseServiceConfig represents configurable properties for a specific component of the SSI Service @@ -84,6 +86,13 @@ type CredentialServiceConfig struct { // TODO(gabe) supported key and signature types } +type KeyStoreServiceConfig struct { + *BaseServiceConfig + // Service key password. Used by a KDF whose key is used by a symmetric cypher for key encryption. + // The password is salted before usage. + ServiceKeyPassword string +} + // LoadConfig attempts to load a TOML config file from the given path, and coerce it into our object model. // Before loading, defaults are applied on certain properties, which are overwritten if specified in the TOML file. func LoadConfig(path string) (*SSIServiceConfig, error) { @@ -134,6 +143,13 @@ func LoadConfig(path string) (*SSIServiceConfig, error) { SchemaConfig: SchemaServiceConfig{ BaseServiceConfig: &BaseServiceConfig{Name: "schema"}, }, + CredentialConfig: CredentialServiceConfig{ + BaseServiceConfig: &BaseServiceConfig{Name: "credential"}, + }, + KeyStoreConfig: KeyStoreServiceConfig{ + BaseServiceConfig: &BaseServiceConfig{Name: "keystore"}, + ServiceKeyPassword: "default-password", + }, } } else { // load from TOML file diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 98e818d8c..6138153a9 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -267,6 +267,17 @@ definitions: status: type: string type: object + github.com_tbd54566975_ssi-service_pkg_server_router.GetKeyDetailsResponse: + properties: + controller: + type: string + createdAt: + type: string + id: + type: string + type: + type: string + type: object github.com_tbd54566975_ssi-service_pkg_server_router.GetSchemaResponse: properties: schema: @@ -279,6 +290,22 @@ definitions: $ref: '#/definitions/schema.VCJSONSchema' type: array type: object + github.com_tbd54566975_ssi-service_pkg_server_router.StoreKeyRequest: + properties: + base58PrivateKey: + type: string + controller: + type: string + id: + type: string + type: + type: string + required: + - base58PrivateKey + - controller + - id + - type + type: object pkg_server_router.CreateCredentialRequest: properties: '@context': @@ -373,6 +400,17 @@ definitions: status: type: string type: object + pkg_server_router.GetKeyDetailsResponse: + properties: + controller: + type: string + createdAt: + type: string + id: + type: string + type: + type: string + type: object pkg_server_router.GetSchemaResponse: properties: schema: @@ -385,6 +423,22 @@ definitions: $ref: '#/definitions/schema.VCJSONSchema' type: array type: object + pkg_server_router.StoreKeyRequest: + properties: + base58PrivateKey: + type: string + controller: + type: string + id: + type: string + type: + type: string + required: + - base58PrivateKey + - controller + - id + - type + type: object schema.JSONSchema: additionalProperties: true type: object @@ -429,7 +483,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.GetHealthCheckResponse' + $ref: '#/definitions/pkg_server_router.GetHealthCheckResponse' summary: Health Check tags: - HealthCheck @@ -580,7 +634,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/pkg_server_router.GetDIDMethodsResponse' + $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.GetDIDMethodsResponse' summary: Get DID Methods tags: - DecentralizedIdentityAPI @@ -595,7 +649,7 @@ paths: name: request required: true schema: - $ref: '#/definitions/pkg_server_router.CreateDIDByMethodRequest' + $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.CreateDIDByMethodRequest' - description: Method in: path name: method @@ -607,7 +661,7 @@ paths: "201": description: Created schema: - $ref: '#/definitions/pkg_server_router.CreateDIDByMethodResponse' + $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.CreateDIDByMethodResponse' "400": description: Bad request schema: @@ -630,7 +684,7 @@ paths: name: request required: true schema: - $ref: '#/definitions/pkg_server_router.CreateDIDByMethodRequest' + $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.CreateDIDByMethodRequest' - description: Method in: path name: method @@ -647,7 +701,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/pkg_server_router.GetDIDByMethodResponse' + $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.GetDIDByMethodResponse' "400": description: Bad request schema: @@ -655,6 +709,59 @@ paths: summary: Get DID tags: - DecentralizedIdentityAPI + /v1/keys: + put: + consumes: + - application/json + description: Stores a key to be used by the service + parameters: + - description: request body + in: body + name: request + required: true + schema: + $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.StoreKeyRequest' + produces: + - application/json + responses: + "201": + description: "" + "400": + description: Bad request + schema: + type: string + "500": + description: Internal server error + schema: + type: string + summary: Store Key + tags: + - KeyStoreAPI + /v1/keys/{id}: + get: + consumes: + - application/json + description: Get details about a stored key + parameters: + - description: ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github.com_tbd54566975_ssi-service_pkg_server_router.GetKeyDetailsResponse' + "400": + description: Bad request + schema: + type: string + summary: Get Details For Key + tags: + - KeyStoreAPI /v1/schemas: get: consumes: diff --git a/go.mod b/go.mod index d8945d39b..b0bd68f1a 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.17 require ( github.com/BurntSushi/toml v1.1.0 - github.com/TBD54566975/ssi-sdk v0.0.0-20220705213209-17514605afb5 + github.com/TBD54566975/ssi-sdk v0.0.0-20220719010135-e2fdcfb80e49 github.com/ardanlabs/conf v1.5.0 github.com/boltdb/bolt v1.3.1 github.com/dimfeld/httptreemux/v5 v5.4.0 @@ -46,7 +46,7 @@ require ( github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect go.opentelemetry.io/otel v1.8.0 // indirect - golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e // indirect + golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 // indirect golang.org/x/text v0.3.7 // indirect gopkg.in/go-playground/assert.v1 v1.2.1 // indirect diff --git a/go.sum b/go.sum index caebe77f9..e4211c0b2 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I= github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/TBD54566975/ssi-sdk v0.0.0-20220705213209-17514605afb5 h1:UDTN4mYVOsFKH8yDs7kljkT4OMZIkmgGuDXOGrsZGq8= -github.com/TBD54566975/ssi-sdk v0.0.0-20220705213209-17514605afb5/go.mod h1:VqKlHw4Oz1ncfavUamyO16VckHMz9APwiYd3GFc/Jgg= +github.com/TBD54566975/ssi-sdk v0.0.0-20220719010135-e2fdcfb80e49 h1:BozkJ4jg1nZyTzBs/ltNq+KtTuveZDh48Ax85cVh0M4= +github.com/TBD54566975/ssi-sdk v0.0.0-20220719010135-e2fdcfb80e49/go.mod h1:uWJkLbobBINP2QFVFL5kku5GtCmbmggzCHh8sTH5NYs= 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.2.2/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= @@ -30,8 +30,6 @@ github.com/go-playground/validator/v10 v10.11.0 h1:0W+xRM511GY47Yy3bZUbJVitCNg2B github.com/go-playground/validator/v10 v10.11.0/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/goccy/go-json v0.9.8/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/goccy/go-json v0.9.8/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.9.10 h1:hCeNmprSNLB8B8vQKWl6DpuH0t60oEs+TAk9a7CScKc= github.com/goccy/go-json v0.9.10/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -126,8 +124,8 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e h1:CsOuNlbOuf0mzxJIefr6Q4uAUetRUwZE4qt7VfzP+xo= -golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 h1:CBpWXWQpIRjzmkkA+M7q9Fqnwd2mZr3AFqexg8YTfoM= golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= diff --git a/internal/util/crypto.go b/internal/util/crypto.go new file mode 100644 index 000000000..9ecdc2128 --- /dev/null +++ b/internal/util/crypto.go @@ -0,0 +1,96 @@ +package util + +import ( + "crypto/rand" + + "github.com/pkg/errors" + "golang.org/x/crypto/argon2" + "golang.org/x/crypto/chacha20poly1305" +) + +const ( + // Argon2SaltSize represents the recommended salt size for argon2, which is 16 bytes + // https://tools.ietf.org/id/draft-irtf-cfrg-argon2-05.html#rfc.section.3.1 + Argon2SaltSize = 16 + + // default parameters from https://pkg.go.dev/golang.org/x/crypto/argon2 + argon2Time = 1 + + // From the godoc: the number of passes over the memory and the + // memory parameter specifies the size of the memory in KiB. For example + // memory=64*1024 sets the memory cost to ~64 MB. The number of threads can be + // adjusted to the numbers of available CPUs. The cost parameters should be + // increased as memory latency and CPU parallelism increases + argon2Memory = 64 * 1024 + threads = 4 +) + +// XChaCha20Poly1305Encrypt takes a 32 byte key and uses XChaCha20-Poly1305 to encrypt a piece of data +func XChaCha20Poly1305Encrypt(key, data []byte) ([]byte, error) { + aead, err := chacha20poly1305.NewX(key) + if err != nil { + return nil, errors.Wrap(err, "could not create aead with provided key") + } + + // generate a random nonce, leaving room for the ciphertext + nonce := make([]byte, aead.NonceSize(), aead.NonceSize()+len(data)+aead.Overhead()) + if _, err := rand.Read(nonce); err != nil { + return nil, errors.Wrap(err, "could not generate nonce for encryption") + } + + encrypted := aead.Seal(nonce, nonce, data, nil) + return encrypted, nil +} + +// XChaCha20Poly1305Decrypt takes a 32 byte key and uses XChaCha20-Poly1305 to decrypt a piece of data +func XChaCha20Poly1305Decrypt(key, data []byte) ([]byte, error) { + aead, err := chacha20poly1305.NewX(key) + if err != nil { + return nil, errors.Wrap(err, "could not create aead with provided key") + } + + if len(data) < aead.NonceSize() { + panic("ciphertext too short") + } + + // split nonce and ciphertext + nonce, ciphertext := data[:aead.NonceSize()], data[aead.NonceSize():] + + // Decrypt the message and check it wasn't tampered with. + decyrpted, err := aead.Open(nil, nonce, ciphertext, nil) + if err != nil { + return nil, errors.Wrap(err, "could not decrypt data") + } + return decyrpted, nil +} + +// Argon2KeyGen returns an encoded string generation of a key generated using the go crypto argon2 impl +// specifically, the Argon2id version, a "hybrid version of Argon2 combining Argon2i and Argon2d." +func Argon2KeyGen(password string, salt []byte, keyLen int) ([]byte, error) { + if password == "" { + return nil, errors.New("password cannot be empty") + } + if len(salt) == 0 { + return nil, errors.New("salt cannot be empty") + } + if keyLen <= 0 { + return nil, errors.New("invalid key length") + } + + key := argon2.IDKey([]byte(password), salt, argon2Time, argon2Memory, threads, uint32(keyLen)) + return key, nil +} + +// GenerateSalt generates a random salt value for a given size +func GenerateSalt(size int) ([]byte, error) { + if size <= 0 { + return nil, errors.New("invalid size") + } + + salt := make([]byte, size) + if _, err := rand.Read(salt[:]); err != nil { + return nil, err + } + + return salt, nil +} diff --git a/internal/util/crypto_test.go b/internal/util/crypto_test.go new file mode 100644 index 000000000..9242cc62a --- /dev/null +++ b/internal/util/crypto_test.go @@ -0,0 +1,48 @@ +package util + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "golang.org/x/crypto/chacha20poly1305" +) + +func TestArgon2(t *testing.T) { + password := "test-password" + salt, err := GenerateSalt(Argon2SaltSize) + assert.NoError(t, err) + assert.NotEmpty(t, salt) + + hash, err := Argon2KeyGen(password, salt, 32) + assert.NoError(t, err) + assert.NotEmpty(t, hash) + + hash2, err := Argon2KeyGen(password, salt, 32) + assert.NoError(t, err) + assert.NotEmpty(t, hash2) + + assert.Equal(t, hash, hash2) +} + +func TestXChaCha20Poly1305(t *testing.T) { + // Generate a key + password := "test-password" + salt, err := GenerateSalt(Argon2SaltSize) + assert.NoError(t, err) + assert.NotEmpty(t, salt) + + key, err := Argon2KeyGen(password, salt, chacha20poly1305.KeySize) + assert.NoError(t, err) + assert.NotEmpty(t, key) + + // Encrypt a message + message := []byte("open sesame") + encrypted, err := XChaCha20Poly1305Encrypt(key, message) + assert.NoError(t, err) + assert.NotEmpty(t, encrypted) + + // Decrypt the message + decrypted, err := XChaCha20Poly1305Decrypt(key, encrypted) + assert.NoError(t, err) + assert.Equal(t, message, decrypted) +} diff --git a/internal/util/util.go b/internal/util/util.go index f24f2ddab..bf7ae6c0b 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -32,12 +32,13 @@ func LoggingNewError(msg string) error { // LoggingErrorMsg is a utility to combine logging an error, and returning and error with a message func LoggingErrorMsg(err error, msg string) error { - logrus.WithError(err).Error(SanitizingLog(msg)) + logrus.WithError(err).Error(SanitizeLog(msg)) return errors.Wrap(err, msg) } -// Sanitizing before it is logged -func SanitizingLog(log string) string { +// SanitizeLog prevents certain classes of injection attacks before logging +// https://codeql.github.com/codeql-query-help/go/go-log-injection/ +func SanitizeLog(log string) string { escapedLog := strings.Replace(log, "\n", "", -1) return strings.Replace(escapedLog, "\r", "", -1) } diff --git a/magefile.go b/magefile.go index 01db0c787..1fb5e45f3 100644 --- a/magefile.go +++ b/magefile.go @@ -34,7 +34,7 @@ func Build() error { // Clean deletes any build artifacts. func Clean() { fmt.Println("Cleaning...") - os.RemoveAll("bin") + _ = os.RemoveAll("bin") } // Run the service via docker-compose diff --git a/pkg/server/router/credential.go b/pkg/server/router/credential.go index 53cfae5b7..d739cf113 100644 --- a/pkg/server/router/credential.go +++ b/pkg/server/router/credential.go @@ -8,6 +8,7 @@ import ( credsdk "github.com/TBD54566975/ssi-sdk/credential" "github.com/pkg/errors" "github.com/sirupsen/logrus" + "github.com/tbd54566975/ssi-service/internal/util" "github.com/tbd54566975/ssi-service/pkg/server/framework" "github.com/tbd54566975/ssi-service/pkg/service/credential" @@ -15,53 +16,53 @@ import ( ) const ( - IssuerParam string = "issuer" - SubjectParam string = "subject" - SchemaParam string = "schema" + IssuerParam string = "issuer" + SubjectParam string = "subject" + SchemaParam string = "schema" ) type CredentialRouter struct { - service *credential.Service + service *credential.Service } func NewCredentialRouter(s svcframework.Service) (*CredentialRouter, error) { - if s == nil { - return nil, errors.New("service cannot be nil") - } - credService, ok := s.(*credential.Service) - if !ok { - return nil, fmt.Errorf("could not create credential router with service type: %s", s.Type()) - } - return &CredentialRouter{ - service: credService, - }, nil + if s == nil { + return nil, errors.New("service cannot be nil") + } + credService, ok := s.(*credential.Service) + if !ok { + return nil, fmt.Errorf("could not create credential router with service type: %s", s.Type()) + } + return &CredentialRouter{ + service: credService, + }, nil } type CreateCredentialRequest struct { - Issuer string `json:"issuer" validate:"required"` - Subject string `json:"subject" validate:"required"` - // 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"` - // TODO(gabe) support more capabilities like signature type, format, status, and more. + Issuer string `json:"issuer" validate:"required"` + Subject string `json:"subject" validate:"required"` + // 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"` + // TODO(gabe) support more capabilities like signature type, format, status, and more. } func (c CreateCredentialRequest) ToServiceRequest() credential.CreateCredentialRequest { - return credential.CreateCredentialRequest{ - Issuer: c.Issuer, - Subject: c.Subject, - Context: c.Context, - JSONSchema: c.Schema, - Data: c.Data, - Expiry: c.Expiry, - } + return credential.CreateCredentialRequest{ + Issuer: c.Issuer, + Subject: c.Subject, + Context: c.Context, + JSONSchema: c.Schema, + Data: c.Data, + Expiry: c.Expiry, + } } type CreateCredentialResponse struct { - Credential credsdk.VerifiableCredential `json:"credential"` + Credential credsdk.VerifiableCredential `json:"credential"` } // CreateCredential godoc @@ -76,28 +77,28 @@ type CreateCredentialResponse struct { // @Failure 500 {string} string "Internal server error" // @Router /v1/credentials [put] func (cr CredentialRouter) CreateCredential(ctx context.Context, w http.ResponseWriter, r *http.Request) error { - var request CreateCredentialRequest - if err := framework.Decode(r, &request); err != nil { - errMsg := "invalid create credential request" - logrus.WithError(err).Error(errMsg) - return framework.NewRequestErrorMsg(errMsg, http.StatusBadRequest) - } - - req := request.ToServiceRequest() - createCredentialResponse, err := cr.service.CreateCredential(req) - if err != nil { - errMsg := "could not create credential" - logrus.WithError(err).Error(errMsg) - return framework.NewRequestErrorMsg(errMsg, http.StatusInternalServerError) - } - - resp := CreateCredentialResponse{Credential: createCredentialResponse.Credential} - return framework.Respond(ctx, w, resp, http.StatusCreated) + var request CreateCredentialRequest + if err := framework.Decode(r, &request); err != nil { + errMsg := "invalid create credential request" + logrus.WithError(err).Error(errMsg) + return framework.NewRequestErrorMsg(errMsg, http.StatusBadRequest) + } + + req := request.ToServiceRequest() + createCredentialResponse, err := cr.service.CreateCredential(req) + if err != nil { + errMsg := "could not create credential" + logrus.WithError(err).Error(errMsg) + return framework.NewRequestErrorMsg(errMsg, http.StatusInternalServerError) + } + + resp := CreateCredentialResponse{Credential: createCredentialResponse.Credential} + return framework.Respond(ctx, w, resp, http.StatusCreated) } type GetCredentialResponse struct { - ID string `json:"id"` - Credential credsdk.VerifiableCredential `json:"credential"` + ID string `json:"id"` + Credential credsdk.VerifiableCredential `json:"credential"` } // GetCredential godoc @@ -111,29 +112,29 @@ type GetCredentialResponse struct { // @Failure 400 {string} string "Bad request" // @Router /v1/credentials/{id} [get] func (cr CredentialRouter) GetCredential(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) - } - - 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.NewRequestErrorMsg(errMsg, http.StatusBadRequest) - } - - resp := GetCredentialResponse{ - ID: gotCredential.Credential.ID, - Credential: gotCredential.Credential, - } - return framework.Respond(ctx, w, resp, http.StatusOK) + 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.NewRequestErrorMsg(errMsg, http.StatusBadRequest) + } + + resp := GetCredentialResponse{ + ID: gotCredential.Credential.ID, + Credential: gotCredential.Credential, + } + return framework.Respond(ctx, w, resp, http.StatusOK) } type GetCredentialsResponse struct { - Credentials []credsdk.VerifiableCredential `json:"credentials"` + Credentials []credsdk.VerifiableCredential `json:"credentials"` } // GetCredentials godoc @@ -150,63 +151,63 @@ type GetCredentialsResponse struct { // @Failure 500 {string} string "Internal server error" // @Router /v1/credentials [get] func (cr CredentialRouter) GetCredentials(ctx context.Context, w http.ResponseWriter, r *http.Request) error { - issuer := framework.GetQueryValue(r, IssuerParam) - schema := framework.GetQueryValue(r, SchemaParam) - subject := framework.GetQueryValue(r, SubjectParam) - - err := framework.NewRequestErrorMsg("must use one of the following query parameters: issuer, subject, schema", http.StatusBadRequest) - - // check if there are multiple parameters set, which is not allowed - if (issuer != nil && subject != nil) || (issuer != nil && schema != nil) || (subject != nil && schema != nil) { - return err - } - - if issuer != nil { - return cr.getCredentialsByIssuer(*issuer, ctx, w, r) - } - if subject != nil { - return cr.getCredentialsBySubject(*subject, ctx, w, r) - } - if schema != nil { - return cr.getCredentialsBySchema(*schema, ctx, w, r) - } - return err + issuer := framework.GetQueryValue(r, IssuerParam) + schema := framework.GetQueryValue(r, SchemaParam) + subject := framework.GetQueryValue(r, SubjectParam) + + err := framework.NewRequestErrorMsg("must use one of the following query parameters: issuer, subject, schema", http.StatusBadRequest) + + // check if there are multiple parameters set, which is not allowed + if (issuer != nil && subject != nil) || (issuer != nil && schema != nil) || (subject != nil && schema != nil) { + return err + } + + if issuer != nil { + return cr.getCredentialsByIssuer(*issuer, ctx, w, r) + } + if subject != nil { + return cr.getCredentialsBySubject(*subject, ctx, w, r) + } + if schema != nil { + return cr.getCredentialsBySchema(*schema, ctx, w, r) + } + return err } func (cr CredentialRouter) getCredentialsByIssuer(issuer string, ctx context.Context, w http.ResponseWriter, r *http.Request) error { - gotCredentials, err := cr.service.GetCredentialsByIssuer(credential.GetCredentialByIssuerRequest{Issuer: issuer}) - if err != nil { - errMsg := fmt.Sprintf("could not get credentials for issuer: %s", util.SanitizingLog(issuer)) - logrus.WithError(err).Error(errMsg) - return framework.NewRequestErrorMsg(errMsg, http.StatusInternalServerError) - } - - resp := GetCredentialsResponse{Credentials: gotCredentials.Credentials} - return framework.Respond(ctx, w, resp, http.StatusOK) + gotCredentials, err := cr.service.GetCredentialsByIssuer(credential.GetCredentialByIssuerRequest{Issuer: issuer}) + if err != nil { + errMsg := fmt.Sprintf("could not get credentials for issuer: %s", util.SanitizeLog(issuer)) + logrus.WithError(err).Error(errMsg) + return framework.NewRequestErrorMsg(errMsg, http.StatusInternalServerError) + } + + resp := GetCredentialsResponse{Credentials: gotCredentials.Credentials} + return framework.Respond(ctx, w, resp, http.StatusOK) } func (cr CredentialRouter) getCredentialsBySubject(subject string, ctx context.Context, w http.ResponseWriter, r *http.Request) error { - gotCredentials, err := cr.service.GetCredentialsBySubject(credential.GetCredentialBySubjectRequest{Subject: subject}) - if err != nil { - errMsg := fmt.Sprintf("could not get credentials for subject: %s", util.SanitizingLog(subject)) - logrus.WithError(err).Error(errMsg) - return framework.NewRequestErrorMsg(errMsg, http.StatusInternalServerError) - } - - resp := GetCredentialsResponse{Credentials: gotCredentials.Credentials} - return framework.Respond(ctx, w, resp, http.StatusOK) + gotCredentials, err := cr.service.GetCredentialsBySubject(credential.GetCredentialBySubjectRequest{Subject: subject}) + if err != nil { + errMsg := fmt.Sprintf("could not get credentials for subject: %s", util.SanitizeLog(subject)) + logrus.WithError(err).Error(errMsg) + return framework.NewRequestErrorMsg(errMsg, http.StatusInternalServerError) + } + + resp := GetCredentialsResponse{Credentials: gotCredentials.Credentials} + return framework.Respond(ctx, w, resp, http.StatusOK) } func (cr CredentialRouter) getCredentialsBySchema(schema string, ctx context.Context, w http.ResponseWriter, r *http.Request) error { - gotCredentials, err := cr.service.GetCredentialsBySchema(credential.GetCredentialBySchemaRequest{Schema: schema}) - if err != nil { - errMsg := fmt.Sprintf("could not get credentials for schema: %s", util.SanitizingLog(schema)) - logrus.WithError(err).Error(errMsg) - return framework.NewRequestErrorMsg(errMsg, http.StatusInternalServerError) - } - - resp := GetCredentialsResponse{Credentials: gotCredentials.Credentials} - return framework.Respond(ctx, w, resp, http.StatusOK) + gotCredentials, err := cr.service.GetCredentialsBySchema(credential.GetCredentialBySchemaRequest{Schema: schema}) + if err != nil { + errMsg := fmt.Sprintf("could not get credentials for schema: %s", util.SanitizeLog(schema)) + logrus.WithError(err).Error(errMsg) + return framework.NewRequestErrorMsg(errMsg, http.StatusInternalServerError) + } + + resp := GetCredentialsResponse{Credentials: gotCredentials.Credentials} + return framework.Respond(ctx, w, resp, http.StatusOK) } // DeleteCredential godoc @@ -221,18 +222,18 @@ func (cr CredentialRouter) getCredentialsBySchema(schema string, ctx context.Con // @Failure 500 {string} string "Internal server error" // @Router /v1/credentials/{id} [delete] func (cr CredentialRouter) DeleteCredential(ctx context.Context, w http.ResponseWriter, r *http.Request) error { - id := framework.GetParam(ctx, IDParam) - if id == nil { - errMsg := "cannot delete credential without ID parameter" - logrus.Error(errMsg) - return framework.NewRequestErrorMsg(errMsg, http.StatusBadRequest) - } - - if err := cr.service.DeleteCredential(credential.DeleteCredentialRequest{ID: *id}); err != nil { - errMsg := fmt.Sprintf("could not delete credential with id: %s", *id) - logrus.WithError(err).Error(errMsg) - return framework.NewRequestErrorMsg(errMsg, http.StatusInternalServerError) - } - - return framework.Respond(ctx, w, nil, http.StatusOK) + id := framework.GetParam(ctx, IDParam) + if id == nil { + errMsg := "cannot delete credential without ID parameter" + logrus.Error(errMsg) + return framework.NewRequestErrorMsg(errMsg, http.StatusBadRequest) + } + + if err := cr.service.DeleteCredential(credential.DeleteCredentialRequest{ID: *id}); err != nil { + errMsg := fmt.Sprintf("could not delete credential with id: %s", *id) + logrus.WithError(err).Error(errMsg) + return framework.NewRequestErrorMsg(errMsg, http.StatusInternalServerError) + } + + return framework.Respond(ctx, w, nil, http.StatusOK) } diff --git a/pkg/server/router/did_test.go b/pkg/server/router/did_test.go index 7c5a0e7ff..248bd027a 100644 --- a/pkg/server/router/did_test.go +++ b/pkg/server/router/did_test.go @@ -1,94 +1,82 @@ package router import ( + "os" + "testing" + "github.com/TBD54566975/ssi-sdk/crypto" "github.com/stretchr/testify/assert" + "github.com/tbd54566975/ssi-service/config" "github.com/tbd54566975/ssi-service/pkg/service/did" "github.com/tbd54566975/ssi-service/pkg/service/framework" "github.com/tbd54566975/ssi-service/pkg/storage" - "os" - "testing" ) func TestDIDRouter(t *testing.T) { - // remove the db file after the test - t.Cleanup(func() { - _ = os.Remove(storage.DBFile) - }) - - t.Run("Nil Service", func(tt *testing.T) { - didRouter, err := NewDIDRouter(nil) - assert.Error(tt, err) - assert.Empty(tt, didRouter) - assert.Contains(tt, err.Error(), "service cannot be nil") - }) - - t.Run("Bad Service", func(tt *testing.T) { - didRouter, err := NewDIDRouter(&testService{}) - assert.Error(tt, err) - assert.Empty(tt, didRouter) - assert.Contains(tt, err.Error(), "could not create DID router with service type: test") - }) - - t.Run("DID Service Test", func(tt *testing.T) { - bolt, err := storage.NewBoltDB() - assert.NoError(tt, err) - assert.NotEmpty(tt, bolt) - - serviceConfig := config.DIDServiceConfig{Methods: []string{string(did.KeyMethod)}} - didService, err := did.NewDIDService(serviceConfig, bolt) - assert.NoError(tt, err) - assert.NotEmpty(tt, didService) - - // check type and status - assert.Equal(tt, framework.DID, didService.Type()) - assert.Equal(tt, framework.StatusReady, didService.Status().Status) - - // get unknown handler - _, err = didService.GetDIDByMethod(did.GetDIDRequest{Method: "bad"}) - assert.Error(tt, err) - assert.Contains(tt, err.Error(), "could not get handler for method") - - supported := didService.GetSupportedMethods() - assert.NotEmpty(tt, supported) - assert.Len(tt, supported.Methods, 1) - assert.Equal(tt, did.KeyMethod, supported.Methods[0]) - - // bad key type - _, err = didService.CreateDIDByMethod(did.CreateDIDRequest{Method: did.KeyMethod, KeyType: "bad"}) - assert.Error(tt, err) - assert.Contains(tt, err.Error(), "could not create did:key") - - // good key type - createDIDResponse, err := didService.CreateDIDByMethod(did.CreateDIDRequest{Method: did.KeyMethod, KeyType: crypto.Ed25519}) - assert.NoError(tt, err) - assert.NotEmpty(tt, createDIDResponse) - - // check the DID is a did:key - assert.Contains(tt, createDIDResponse.DID.ID, "did:key") - - // get it back - getDIDResponse, err := didService.GetDIDByMethod(did.GetDIDRequest{Method: did.KeyMethod, ID: createDIDResponse.DID.ID}) - assert.NoError(tt, err) - assert.NotEmpty(tt, getDIDResponse) - - // make sure it's the same value - assert.Equal(tt, createDIDResponse.DID.ID, getDIDResponse.DID.ID) - }) -} - -type testService struct{} - -func (s *testService) Type() framework.Type { - return "test" -} - -func (s *testService) Status() framework.Status { - return framework.Status{Status: "ready"} -} - -func (s *testService) Config() config.DIDServiceConfig { - return config.DIDServiceConfig{Methods: []string{string(did.KeyMethod)}} + // remove the db file after the test + t.Cleanup(func() { + _ = os.Remove(storage.DBFile) + }) + + t.Run("Nil Service", func(tt *testing.T) { + didRouter, err := NewDIDRouter(nil) + assert.Error(tt, err) + assert.Empty(tt, didRouter) + assert.Contains(tt, err.Error(), "service cannot be nil") + }) + + t.Run("Bad Service", func(tt *testing.T) { + didRouter, err := NewDIDRouter(&testService{}) + assert.Error(tt, err) + assert.Empty(tt, didRouter) + assert.Contains(tt, err.Error(), "could not create DID router with service type: test") + }) + + t.Run("DID Service Test", func(tt *testing.T) { + bolt, err := storage.NewBoltDB() + assert.NoError(tt, err) + assert.NotEmpty(tt, bolt) + + serviceConfig := config.DIDServiceConfig{Methods: []string{string(did.KeyMethod)}} + didService, err := did.NewDIDService(serviceConfig, bolt) + assert.NoError(tt, err) + assert.NotEmpty(tt, didService) + + // check type and status + assert.Equal(tt, framework.DID, didService.Type()) + assert.Equal(tt, framework.StatusReady, didService.Status().Status) + + // get unknown handler + _, err = didService.GetDIDByMethod(did.GetDIDRequest{Method: "bad"}) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "could not get handler for method") + + supported := didService.GetSupportedMethods() + assert.NotEmpty(tt, supported) + assert.Len(tt, supported.Methods, 1) + assert.Equal(tt, did.KeyMethod, supported.Methods[0]) + + // bad key type + _, err = didService.CreateDIDByMethod(did.CreateDIDRequest{Method: did.KeyMethod, KeyType: "bad"}) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "could not create did:key") + + // good key type + createDIDResponse, err := didService.CreateDIDByMethod(did.CreateDIDRequest{Method: did.KeyMethod, KeyType: crypto.Ed25519}) + assert.NoError(tt, err) + assert.NotEmpty(tt, createDIDResponse) + + // check the DID is a did:key + assert.Contains(tt, createDIDResponse.DID.ID, "did:key") + + // get it back + getDIDResponse, err := didService.GetDIDByMethod(did.GetDIDRequest{Method: did.KeyMethod, ID: createDIDResponse.DID.ID}) + assert.NoError(tt, err) + assert.NotEmpty(tt, getDIDResponse) + + // make sure it's the same value + assert.Equal(tt, createDIDResponse.DID.ID, getDIDResponse.DID.ID) + }) } diff --git a/pkg/server/router/keystore.go b/pkg/server/router/keystore.go new file mode 100644 index 000000000..c7b9b0a05 --- /dev/null +++ b/pkg/server/router/keystore.go @@ -0,0 +1,127 @@ +package router + +import ( + "context" + "fmt" + "net/http" + + "github.com/TBD54566975/ssi-sdk/crypto" + "github.com/mr-tron/base58" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + + "github.com/tbd54566975/ssi-service/pkg/server/framework" + svcframework "github.com/tbd54566975/ssi-service/pkg/service/framework" + "github.com/tbd54566975/ssi-service/pkg/service/keystore" +) + +type KeyStoreRouter struct { + service *keystore.Service +} + +func NewKeyStoreRouter(s svcframework.Service) (*KeyStoreRouter, error) { + if s == nil { + return nil, errors.New("service cannot be nil") + } + keyStoreService, ok := s.(*keystore.Service) + if !ok { + return nil, fmt.Errorf("could not create key store router with service type: %s", s.Type()) + } + return &KeyStoreRouter{service: keyStoreService}, nil +} + +type StoreKeyRequest struct { + ID string `json:"id" validate:"required"` + Type crypto.KeyType `json:"type,omitempty" validate:"required"` + Controller string `json:"controller,omitempty" validate:"required"` + Base58PrivateKey string `json:"base58PrivateKey,omitempty" validate:"required"` +} + +func (sk StoreKeyRequest) ToServiceRequest() (*keystore.StoreKeyRequest, error) { + privateKeyBytes, err := base58.Decode(sk.Base58PrivateKey) + if err != nil { + return nil, errors.Wrap(err, "could not decode base58 private key") + } + return &keystore.StoreKeyRequest{ + ID: sk.ID, + Type: sk.Type, + Controller: sk.Controller, + Key: privateKeyBytes, + }, nil +} + +// StoreKey godoc +// @Summary Store Key +// @Description Stores a key to be used by the service +// @Tags KeyStoreAPI +// @Accept json +// @Produce json +// @Param request body StoreKeyRequest true "request body" +// @Success 201 +// @Failure 400 {string} string "Bad request" +// @Failure 500 {string} string "Internal server error" +// @Router /v1/keys [put] +func (ksr *KeyStoreRouter) StoreKey(ctx context.Context, w http.ResponseWriter, r *http.Request) error { + var request StoreKeyRequest + if err := framework.Decode(r, &request); err != nil { + errMsg := "invalid store key request" + logrus.WithError(err).Error(errMsg) + return framework.NewRequestErrorMsg(errMsg, http.StatusBadRequest) + } + + req, err := request.ToServiceRequest() + if err != nil { + errMsg := "could not process store key request" + logrus.WithError(err).Error(errMsg) + return framework.NewRequestErrorMsg(errMsg, http.StatusBadRequest) + } + + if err := ksr.service.StoreKey(*req); err != nil { + errMsg := fmt.Sprintf("could not store key: %s, %s", request.ID, err.Error()) + logrus.WithError(err).Error(errMsg) + return framework.NewRequestErrorMsg(errMsg, http.StatusInternalServerError) + } + + return framework.Respond(ctx, w, nil, http.StatusCreated) +} + +type GetKeyDetailsResponse struct { + ID string `json:"id,omitempty"` + Type crypto.KeyType `json:"type,omitempty"` + Controller string `json:"controller,omitempty"` + CreatedAt string `json:"createdAt,omitempty"` +} + +// GetKeyDetails godoc +// @Summary Get Details For Key +// @Description Get details about a stored key +// @Tags KeyStoreAPI +// @Accept json +// @Produce json +// @Param id path string true "ID" +// @Success 200 {object} GetKeyDetailsResponse +// @Failure 400 {string} string "Bad request" +// @Router /v1/keys/{id} [get] +func (ksr *KeyStoreRouter) GetKeyDetails(ctx context.Context, w http.ResponseWriter, _ *http.Request) error { + id := framework.GetParam(ctx, IDParam) + if id == nil { + errMsg := "cannot get key details without ID parameter" + logrus.Error(errMsg) + return framework.NewRequestErrorMsg(errMsg, http.StatusBadRequest) + } + + gotKeyDetails, err := ksr.service.GetKeyDetails(keystore.GetKeyDetailsRequest{ID: *id}) + if err != nil { + errMsg := fmt.Sprintf("could not get key details for id: %s", *id) + logrus.WithError(err).Error(errMsg) + return framework.NewRequestErrorMsg(errMsg, http.StatusBadRequest) + } + + resp := GetKeyDetailsResponse{ + ID: gotKeyDetails.ID, + Type: gotKeyDetails.Type, + Controller: gotKeyDetails.Controller, + CreatedAt: gotKeyDetails.CreatedAt, + } + return framework.Respond(ctx, w, resp, http.StatusOK) +} diff --git a/pkg/server/router/keystore_test.go b/pkg/server/router/keystore_test.go new file mode 100644 index 000000000..a5bb87e36 --- /dev/null +++ b/pkg/server/router/keystore_test.go @@ -0,0 +1,97 @@ +package router + +import ( + "os" + "testing" + + "github.com/TBD54566975/ssi-sdk/crypto" + "github.com/stretchr/testify/assert" + + "github.com/tbd54566975/ssi-service/config" + "github.com/tbd54566975/ssi-service/pkg/service/framework" + "github.com/tbd54566975/ssi-service/pkg/service/keystore" + "github.com/tbd54566975/ssi-service/pkg/storage" +) + +func TestKeyStoreRouter(t *testing.T) { + + // remove the db file after the test + t.Cleanup(func() { + _ = os.Remove(storage.DBFile) + }) + + t.Run("Nil Service", func(tt *testing.T) { + keyStoreRouter, err := NewKeyStoreRouter(nil) + assert.Error(tt, err) + assert.Empty(tt, keyStoreRouter) + assert.Contains(tt, err.Error(), "service cannot be nil") + }) + + t.Run("Bad Service", func(tt *testing.T) { + keyStoreRouter, err := NewKeyStoreRouter(&testService{}) + assert.Error(tt, err) + assert.Empty(tt, keyStoreRouter) + assert.Contains(tt, err.Error(), "could not create key store router with service type: test") + }) + + t.Run("Key Store Service Test", func(tt *testing.T) { + bolt, err := storage.NewBoltDB() + assert.NoError(tt, err) + assert.NotEmpty(tt, bolt) + + serviceConfig := config.KeyStoreServiceConfig{ + BaseServiceConfig: &config.BaseServiceConfig{Name: "keystore"}, + ServiceKeyPassword: "test-password", + } + keyStoreService, err := keystore.NewKeyStoreService(serviceConfig, bolt) + assert.NoError(tt, err) + assert.NotEmpty(tt, keyStoreService) + + // check type and status + assert.Equal(tt, framework.KeyStore, keyStoreService.Type()) + assert.Equal(tt, framework.StatusReady, keyStoreService.Status().Status) + + // store an invalid key type + err = keyStoreService.StoreKey(keystore.StoreKeyRequest{ + ID: "test-kid", + Type: "bad", + Controller: "me", + Key: []byte("bad"), + }) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "unsupported key type: bad") + + // store a valid key + _, privKey, err := crypto.GenerateKeyByKeyType(crypto.Ed25519) + assert.NoError(tt, err) + assert.NotEmpty(tt, privKey) + + privKeyBytes, err := crypto.PrivKeyToBytes(privKey) + assert.NoError(tt, err) + + keyID := "did:test:me#key-1" + err = keyStoreService.StoreKey(keystore.StoreKeyRequest{ + ID: keyID, + Type: crypto.Ed25519, + Controller: "did:test:me", + Key: privKeyBytes, + }) + assert.NoError(tt, err) + + // get a key that doesn't exist + gotDetails, err := keyStoreService.GetKeyDetails(keystore.GetKeyDetailsRequest{ID: "bad"}) + assert.Error(tt, err) + assert.Empty(tt, gotDetails) + assert.Contains(tt, err.Error(), "could not get key details for key: bad") + + // get a key that exists + gotDetails, err = keyStoreService.GetKeyDetails(keystore.GetKeyDetailsRequest{ID: keyID}) + assert.NoError(tt, err) + assert.NotEmpty(tt, gotDetails) + + // make sure the details match + assert.Equal(tt, keyID, gotDetails.ID) + assert.Equal(tt, crypto.Ed25519, gotDetails.Type) + assert.Equal(tt, "did:test:me", gotDetails.Controller) + }) +} diff --git a/pkg/server/router/router_test.go b/pkg/server/router/router_test.go new file mode 100644 index 000000000..5d29aaa43 --- /dev/null +++ b/pkg/server/router/router_test.go @@ -0,0 +1,29 @@ +package router + +import ( + "github.com/tbd54566975/ssi-service/config" + "github.com/tbd54566975/ssi-service/pkg/service/did" + "github.com/tbd54566975/ssi-service/pkg/service/framework" +) + +// generic test config to be used by all tests in this package + +type testService struct{} + +func (s *testService) Type() framework.Type { + return "test" +} + +func (s *testService) Status() framework.Status { + return framework.Status{Status: "ready"} +} + +func (s *testService) Config() config.ServicesConfig { + return config.ServicesConfig{ + StorageProvider: "bolt", + DIDConfig: config.DIDServiceConfig{Methods: []string{string(did.KeyMethod)}}, + SchemaConfig: config.SchemaServiceConfig{}, + CredentialConfig: config.CredentialServiceConfig{}, + KeyStoreConfig: config.KeyStoreServiceConfig{}, + } +} diff --git a/pkg/server/server.go b/pkg/server/server.go index 1308de8bf..9120edd6e 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -9,6 +9,7 @@ import ( "path" "github.com/sirupsen/logrus" + "github.com/tbd54566975/ssi-service/config" "github.com/tbd54566975/ssi-service/internal/util" "github.com/tbd54566975/ssi-service/pkg/server/framework" @@ -25,6 +26,7 @@ const ( DIDsPrefix = "/dids" SchemasPrefix = "/schemas" CredentialsPrefix = "/credentials" + KeyStorePrefix = "/keys" ) // SSIServer exposes all dependencies needed to run a http server and all its services @@ -82,6 +84,8 @@ func (s *SSIServer) instantiateRouter(service svcframework.Service) error { return s.SchemaAPI(service) case svcframework.Credential: return s.CredentialAPI(service) + case svcframework.KeyStore: + return s.KeyStoreAPI(service) default: return fmt.Errorf("could not instantiate API for service: %s", serviceType) } @@ -131,3 +135,16 @@ func (s *SSIServer) CredentialAPI(service svcframework.Service) (err error) { s.Handle(http.MethodDelete, path.Join(handlerPath, "/:id"), credRouter.DeleteCredential) return } + +func (s *SSIServer) KeyStoreAPI(service svcframework.Service) (err error) { + keyStoreRouter, err := router.NewKeyStoreRouter(service) + if err != nil { + return util.LoggingErrorMsg(err, "could not create key store router") + } + + handlerPath := V1Prefix + KeyStorePrefix + + s.Handle(http.MethodPut, handlerPath, keyStoreRouter.StoreKey) + s.Handle(http.MethodGet, path.Join(handlerPath, "/:id"), keyStoreRouter.GetKeyDetails) + return +} diff --git a/pkg/server/server_test.go b/pkg/server/server_test.go index af0058e47..2966d896e 100644 --- a/pkg/server/server_test.go +++ b/pkg/server/server_test.go @@ -16,14 +16,17 @@ import ( "github.com/dimfeld/httptreemux/v5" "github.com/goccy/go-json" "github.com/google/uuid" + "github.com/mr-tron/base58" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/tbd54566975/ssi-service/config" "github.com/tbd54566975/ssi-service/pkg/server/framework" "github.com/tbd54566975/ssi-service/pkg/server/router" "github.com/tbd54566975/ssi-service/pkg/service/credential" "github.com/tbd54566975/ssi-service/pkg/service/did" svcframework "github.com/tbd54566975/ssi-service/pkg/service/framework" + "github.com/tbd54566975/ssi-service/pkg/service/keystore" "github.com/tbd54566975/ssi-service/pkg/service/schema" "github.com/tbd54566975/ssi-service/pkg/storage" ) @@ -88,18 +91,21 @@ func TestReadinessAPI(t *testing.T) { func TestDIDAPI(t *testing.T) { t.Run("Test Get DID Methods", func(tt *testing.T) { + bolt, err := storage.NewBoltDB() + // remove the db file after the test tt.Cleanup(func() { + _ = bolt.Close() _ = os.Remove(storage.DBFile) }) - didService := newDIDService(tt) + didService := newDIDService(tt, bolt) // get DID methods req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids", nil) w := httptest.NewRecorder() - err := didService.GetDIDMethods(newRequestContext(), w, req) + err = didService.GetDIDMethods(newRequestContext(), w, req) assert.NoError(tt, err) assert.Equal(tt, http.StatusOK, w.Result().StatusCode) @@ -112,12 +118,15 @@ func TestDIDAPI(t *testing.T) { }) t.Run("Test Create DID By Method: Key", func(tt *testing.T) { + bolt, err := storage.NewBoltDB() + // remove the db file after the test tt.Cleanup(func() { + _ = bolt.Close() _ = os.Remove(storage.DBFile) }) - didService := newDIDService(tt) + didService := newDIDService(tt, bolt) // create DID by method - key - missing body req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dids/key", nil) @@ -126,7 +135,7 @@ func TestDIDAPI(t *testing.T) { "method": "key", } - err := didService.CreateDIDByMethod(newRequestContextWithParams(params), w, req) + err = didService.CreateDIDByMethod(newRequestContextWithParams(params), w, req) assert.Error(tt, err) assert.Contains(tt, err.Error(), "invalid create DID request") @@ -157,12 +166,15 @@ func TestDIDAPI(t *testing.T) { }) t.Run("Test Get DID By Method", func(tt *testing.T) { + bolt, err := storage.NewBoltDB() + // remove the db file after the test tt.Cleanup(func() { + _ = bolt.Close() _ = os.Remove(storage.DBFile) }) - didService := newDIDService(tt) + didService := newDIDService(tt, bolt) // get DID by method req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/dids/bad/worse", nil) @@ -173,7 +185,7 @@ func TestDIDAPI(t *testing.T) { "method": "bad", "id": "worse", } - err := didService.GetDIDByMethod(newRequestContextWithParams(badParams), w, req) + err = didService.GetDIDByMethod(newRequestContextWithParams(badParams), w, req) assert.Error(tt, err) assert.Contains(tt, err.Error(), "could not get DID for method") @@ -221,12 +233,7 @@ func TestDIDAPI(t *testing.T) { }) } -func newDIDService(t *testing.T) *router.DIDRouter { - // set up DID service - bolt, err := storage.NewBoltDB() - require.NoError(t, err) - require.NotEmpty(t, bolt) - +func newDIDService(t *testing.T, bolt *storage.BoltDB) *router.DIDRouter { serviceConfig := config.DIDServiceConfig{Methods: []string{string(did.KeyMethod)}} didService, err := did.NewDIDService(serviceConfig, bolt) require.NoError(t, err) @@ -242,12 +249,15 @@ func newDIDService(t *testing.T) *router.DIDRouter { func TestSchemaAPI(t *testing.T) { t.Run("Test Create Schema", func(tt *testing.T) { + bolt, err := storage.NewBoltDB() + // remove the db file after the test tt.Cleanup(func() { + _ = bolt.Close() _ = os.Remove(storage.DBFile) }) - schemaService := newSchemaService(tt) + schemaService := newSchemaService(tt, bolt) simpleSchema := map[string]interface{}{ "type": "object", @@ -264,7 +274,7 @@ func TestSchemaAPI(t *testing.T) { req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/schemas", schemaRequestValue) w := httptest.NewRecorder() - err := schemaService.CreateSchema(newRequestContext(), w, req) + err = schemaService.CreateSchema(newRequestContext(), w, req) assert.Error(tt, err) assert.Contains(tt, err.Error(), "invalid create schema request") @@ -286,17 +296,20 @@ func TestSchemaAPI(t *testing.T) { }) t.Run("Test Get Schemas", func(tt *testing.T) { + bolt, err := storage.NewBoltDB() + // remove the db file after the test tt.Cleanup(func() { + _ = bolt.Close() _ = os.Remove(storage.DBFile) }) - schemaService := newSchemaService(tt) + schemaService := newSchemaService(tt, bolt) // get schema that doesn't exist w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/schemas/bad", nil) - err := schemaService.GetSchemaByID(newRequestContext(), w, req) + err = schemaService.GetSchemaByID(newRequestContext(), w, req) assert.Error(tt, err) assert.Contains(tt, err.Error(), "cannot get schema without ID parameter") @@ -377,12 +390,7 @@ func TestSchemaAPI(t *testing.T) { }) } -func newSchemaService(t *testing.T) *router.SchemaRouter { - // set up schema service - bolt, err := storage.NewBoltDB() - require.NoError(t, err) - require.NotEmpty(t, bolt) - +func newSchemaService(t *testing.T, bolt *storage.BoltDB) *router.SchemaRouter { schemaService, err := schema.NewSchemaService(config.SchemaServiceConfig{}, bolt) require.NoError(t, err) require.NotEmpty(t, schemaService) @@ -397,12 +405,15 @@ func newSchemaService(t *testing.T) *router.SchemaRouter { func TestCredentialAPI(t *testing.T) { t.Run("Test Create Credential", func(tt *testing.T) { + bolt, err := storage.NewBoltDB() + // remove the db file after the test tt.Cleanup(func() { + _ = bolt.Close() _ = os.Remove(storage.DBFile) }) - credService := newCredentialService(tt) + credService := newCredentialService(tt, bolt) // missing required field: data badCredRequest := router.CreateCredentialRequest{ @@ -414,7 +425,7 @@ func TestCredentialAPI(t *testing.T) { req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials", badRequestValue) w := httptest.NewRecorder() - err := credService.CreateCredential(newRequestContext(), w, req) + err = credService.CreateCredential(newRequestContext(), w, req) assert.Error(tt, err) assert.Contains(tt, err.Error(), "invalid create credential request") @@ -445,18 +456,21 @@ func TestCredentialAPI(t *testing.T) { }) t.Run("Test Get Credential By ID", func(tt *testing.T) { + bolt, err := storage.NewBoltDB() + // remove the db file after the test tt.Cleanup(func() { + _ = bolt.Close() _ = os.Remove(storage.DBFile) }) - credService := newCredentialService(tt) + credService := newCredentialService(tt, bolt) w := httptest.NewRecorder() // get a cred that doesn't exit req := httptest.NewRequest(http.MethodGet, "https://ssi-service.com/v1/credentials/bad", nil) - err := credService.GetCredential(newRequestContext(), w, req) + err = credService.GetCredential(newRequestContext(), w, req) assert.Error(tt, err) assert.Contains(tt, err.Error(), "cannot get credential without ID parameter") @@ -503,12 +517,15 @@ func TestCredentialAPI(t *testing.T) { }) t.Run("Test Get Credential By Schema", func(tt *testing.T) { + bolt, err := storage.NewBoltDB() + // remove the db file after the test tt.Cleanup(func() { + _ = bolt.Close() _ = os.Remove(storage.DBFile) }) - credService := newCredentialService(tt) + credService := newCredentialService(tt, bolt) w := httptest.NewRecorder() @@ -525,13 +542,15 @@ func TestCredentialAPI(t *testing.T) { } requestValue := newRequestValue(tt, createCredRequest) req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials", requestValue) - err := credService.CreateCredential(newRequestContext(), w, req) + err = credService.CreateCredential(newRequestContext(), w, req) assert.NoError(tt, err) var resp router.CreateCredentialResponse err = json.NewDecoder(w.Body).Decode(&resp) assert.NoError(tt, err) + w.Flush() + // get credential by schema req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/credential?schema=%s", schemaID), nil) err = credService.GetCredentials(newRequestContext(), w, req) @@ -547,12 +566,15 @@ func TestCredentialAPI(t *testing.T) { }) t.Run("Test Get Credential By Issuer", func(tt *testing.T) { + bolt, err := storage.NewBoltDB() + // remove the db file after the test tt.Cleanup(func() { + _ = bolt.Close() _ = os.Remove(storage.DBFile) }) - credService := newCredentialService(tt) + credService := newCredentialService(tt, bolt) w := httptest.NewRecorder() @@ -568,13 +590,15 @@ func TestCredentialAPI(t *testing.T) { } requestValue := newRequestValue(tt, createCredRequest) req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials", requestValue) - err := credService.CreateCredential(newRequestContext(), w, req) + err = credService.CreateCredential(newRequestContext(), w, req) assert.NoError(tt, err) var resp router.CreateCredentialResponse err = json.NewDecoder(w.Body).Decode(&resp) assert.NoError(tt, err) + w.Flush() + // get credential by issuer id req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/credential?issuer=%s", issuerID), nil) err = credService.GetCredentials(newRequestContext(), w, req) @@ -590,12 +614,15 @@ func TestCredentialAPI(t *testing.T) { }) t.Run("Test Get Credential By Subject", func(tt *testing.T) { + bolt, err := storage.NewBoltDB() + // remove the db file after the test tt.Cleanup(func() { + _ = bolt.Close() _ = os.Remove(storage.DBFile) }) - credService := newCredentialService(tt) + credService := newCredentialService(tt, bolt) w := httptest.NewRecorder() @@ -611,7 +638,7 @@ func TestCredentialAPI(t *testing.T) { } requestValue := newRequestValue(tt, createCredRequest) req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials", requestValue) - err := credService.CreateCredential(newRequestContext(), w, req) + err = credService.CreateCredential(newRequestContext(), w, req) assert.NoError(tt, err) var resp router.CreateCredentialResponse @@ -633,7 +660,15 @@ func TestCredentialAPI(t *testing.T) { }) t.Run("Test Delete Credential", func(tt *testing.T) { - credService := newCredentialService(tt) + bolt, err := storage.NewBoltDB() + + // remove the db file after the test + tt.Cleanup(func() { + _ = bolt.Close() + _ = os.Remove(storage.DBFile) + }) + + credService := newCredentialService(tt, bolt) createCredRequest := router.CreateCredentialRequest{ Issuer: "did:abc:123", @@ -647,13 +682,15 @@ func TestCredentialAPI(t *testing.T) { requestValue := newRequestValue(tt, createCredRequest) req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/credentials", requestValue) w := httptest.NewRecorder() - err := credService.CreateCredential(newRequestContext(), w, req) + err = credService.CreateCredential(newRequestContext(), w, req) assert.NoError(tt, err) var resp router.CreateCredentialResponse err = json.NewDecoder(w.Body).Decode(&resp) assert.NoError(tt, err) + w.Flush() + // get credential by id req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/credentials/%s", resp.Credential.ID), nil) err = credService.GetCredential(newRequestContextWithParams(map[string]string{"id": resp.Credential.ID}), w, req) @@ -665,11 +702,15 @@ func TestCredentialAPI(t *testing.T) { assert.NotEmpty(tt, getCredResp) assert.Equal(tt, resp.Credential.ID, getCredResp.ID) + w.Flush() + // delete it req = httptest.NewRequest(http.MethodDelete, fmt.Sprintf("https://ssi-service.com/v1/credentials/%s", resp.Credential.ID), nil) err = credService.DeleteCredential(newRequestContextWithParams(map[string]string{"id": resp.Credential.ID}), w, req) assert.NoError(tt, err) + w.Flush() + // get it back req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/credentials/%s", resp.Credential.ID), nil) err = credService.GetCredential(newRequestContextWithParams(map[string]string{"id": resp.Credential.ID}), w, req) @@ -678,12 +719,7 @@ func TestCredentialAPI(t *testing.T) { }) } -func newCredentialService(t *testing.T) *router.CredentialRouter { - // set up credential service - bolt, err := storage.NewBoltDB() - require.NoError(t, err) - require.NotEmpty(t, bolt) - +func newCredentialService(t *testing.T, bolt *storage.BoltDB) *router.CredentialRouter { credentialService, err := credential.NewCredentialService(config.CredentialServiceConfig{}, bolt) require.NoError(t, err) require.NotEmpty(t, credentialService) @@ -696,6 +732,119 @@ func newCredentialService(t *testing.T) *router.CredentialRouter { return credentialRouter } +func TestKeyStoreAPI(t *testing.T) { + t.Run("Test Store Key", func(tt *testing.T) { + bolt, err := storage.NewBoltDB() + + // remove the db file after the test + tt.Cleanup(func() { + _ = bolt.Close() + _ = os.Remove(storage.DBFile) + }) + + keyStoreService := newKeyStoreService(tt, bolt) + w := httptest.NewRecorder() + + // bad key type + badKeyStoreRequest := router.StoreKeyRequest{ + ID: "test-kid", + Type: "bad", + Controller: "me", + Base58PrivateKey: "bad", + } + badRequestValue := newRequestValue(tt, badKeyStoreRequest) + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/keys", badRequestValue) + err = keyStoreService.StoreKey(newRequestContext(), w, req) + assert.Error(tt, err) + assert.Contains(tt, err.Error(), "could not store key: test-kid, unsupported key type: bad") + + // reset the http recorder + w.Flush() + + // store a valid key + _, privKey, err := crypto.GenerateKeyByKeyType(crypto.Ed25519) + assert.NoError(tt, err) + assert.NotEmpty(tt, privKey) + + privKeyBytes, err := crypto.PrivKeyToBytes(privKey) + assert.NoError(tt, err) + + // good request + storeKeyRequest := router.StoreKeyRequest{ + ID: "did:test:me#key-1", + Type: crypto.Ed25519, + Controller: "did:test:me", + Base58PrivateKey: base58.Encode(privKeyBytes), + } + requestValue := newRequestValue(tt, storeKeyRequest) + req = httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/keys", requestValue) + err = keyStoreService.StoreKey(newRequestContext(), w, req) + assert.NoError(tt, err) + }) + + t.Run("Test Get Key Details", func(tt *testing.T) { + bolt, err := storage.NewBoltDB() + + // remove the db file after the test + tt.Cleanup(func() { + _ = bolt.Close() + _ = os.Remove(storage.DBFile) + }) + + keyStoreService := newKeyStoreService(tt, bolt) + w := httptest.NewRecorder() + + // store a valid key + _, privKey, err := crypto.GenerateKeyByKeyType(crypto.Ed25519) + assert.NoError(tt, err) + assert.NotEmpty(tt, privKey) + + privKeyBytes, err := crypto.PrivKeyToBytes(privKey) + assert.NoError(tt, err) + + // good request + keyID := "did:test:me#key-2" + controller := "did:test:me" + storeKeyRequest := router.StoreKeyRequest{ + ID: keyID, + Type: crypto.Ed25519, + Controller: controller, + Base58PrivateKey: base58.Encode(privKeyBytes), + } + requestValue := newRequestValue(tt, storeKeyRequest) + req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/keys", requestValue) + err = keyStoreService.StoreKey(newRequestContext(), w, req) + assert.NoError(tt, err) + + // get it back + getRecorder := httptest.NewRecorder() + getReq := httptest.NewRequest(http.MethodGet, fmt.Sprintf("https://ssi-service.com/v1/keys/%s", keyID), nil) + err = keyStoreService.GetKeyDetails(newRequestContextWithParams(map[string]string{"id": keyID}), getRecorder, getReq) + assert.NoError(tt, err) + + var resp router.GetKeyDetailsResponse + err = json.NewDecoder(getRecorder.Body).Decode(&resp) + assert.NoError(tt, err) + assert.Equal(tt, keyID, resp.ID) + assert.Equal(tt, controller, resp.Controller) + assert.Equal(tt, crypto.Ed25519, resp.Type) + }) +} + +func newKeyStoreService(t *testing.T, bolt *storage.BoltDB) *router.KeyStoreRouter { + serviceConfig := config.KeyStoreServiceConfig{ServiceKeyPassword: "test-password"} + keyStoreService, err := keystore.NewKeyStoreService(serviceConfig, bolt) + require.NoError(t, err) + require.NotEmpty(t, keyStoreService) + + // create router for service + keyStoreRouter, err := router.NewKeyStoreRouter(keyStoreService) + require.NoError(t, err) + require.NotEmpty(t, keyStoreRouter) + + return keyStoreRouter +} + func newRequestValue(t *testing.T, data interface{}) io.Reader { dataBytes, err := json.Marshal(data) require.NoError(t, err) diff --git a/pkg/service/credential/credential.go b/pkg/service/credential/credential.go index faf271e74..fe95e2787 100644 --- a/pkg/service/credential/credential.go +++ b/pkg/service/credential/credential.go @@ -6,6 +6,7 @@ import ( "github.com/TBD54566975/ssi-sdk/credential" "github.com/sirupsen/logrus" + "github.com/tbd54566975/ssi-service/config" "github.com/tbd54566975/ssi-service/internal/util" credstorage "github.com/tbd54566975/ssi-service/pkg/service/credential/storage" @@ -14,204 +15,204 @@ import ( ) type Service struct { - storage credstorage.Storage - config config.CredentialServiceConfig + storage credstorage.Storage + config config.CredentialServiceConfig } func (s Service) Type() framework.Type { - return framework.Credential + return framework.Credential } func (s Service) Status() framework.Status { - if s.storage == nil { - return framework.Status{ - Status: framework.StatusNotReady, - Message: "no storage", - } - } - return framework.Status{Status: framework.StatusReady} + if s.storage == nil { + return framework.Status{ + Status: framework.StatusNotReady, + Message: "no storage", + } + } + return framework.Status{Status: framework.StatusReady} } func (s Service) Config() config.CredentialServiceConfig { - return s.config + return s.config } func NewCredentialService(config config.CredentialServiceConfig, s storage.ServiceStorage) (*Service, error) { - credentialStorage, err := credstorage.NewCredentialStorage(s) - if err != nil { - errMsg := "could not instantiate storage for the credential service" - return nil, util.LoggingErrorMsg(err, errMsg) - } - return &Service{ - storage: credentialStorage, - config: config, - }, nil + credentialStorage, err := credstorage.NewCredentialStorage(s) + if err != nil { + errMsg := "could not instantiate storage for the credential service" + return nil, util.LoggingErrorMsg(err, errMsg) + } + return &Service{ + storage: credentialStorage, + config: config, + }, nil } func (s Service) CreateCredential(request CreateCredentialRequest) (*CreateCredentialResponse, error) { - logrus.Debugf("creating credential: %+v", request) - - builder := credential.NewVerifiableCredentialBuilder() - - if err := builder.SetIssuer(request.Issuer); err != nil { - errMsg := fmt.Sprintf("could not build credential when setting issuer: %s", request.Issuer) - return nil, util.LoggingErrorMsg(err, errMsg) - } - - // check if there's a conflict with subject ID - if id, ok := request.Data[credential.VerifiableCredentialIDProperty]; ok && id != request.Subject { - errMsg := fmt.Sprintf("cannot set subject<%s>, data already contains a different ID value: %s", request.Subject, id) - logrus.Error(errMsg) - return nil, util.LoggingNewError(errMsg) - } - - // set subject value - subject := credential.CredentialSubject(request.Data) - subject[credential.VerifiableCredentialIDProperty] = request.Subject - - if err := builder.SetCredentialSubject(subject); err != nil { - errMsg := fmt.Sprintf("could not set subject: %+v", subject) - return nil, util.LoggingErrorMsg(err, errMsg) - } - - // if a context value exists, set it - if request.Context != "" { - if err := builder.AddContext(request.Context); err != nil { - errMsg := fmt.Sprintf("could not add context to credential: %s", request.Context) - return nil, util.LoggingErrorMsg(err, errMsg) - } - } - - // if a schema value exists, set it - if request.JSONSchema != "" { - schema := credential.CredentialSchema{ - ID: request.JSONSchema, - Type: SchemaType, - } - if err := builder.SetCredentialSchema(schema); err != nil { - errMsg := fmt.Sprintf("could not set JSON Schema for credential: %s", request.JSONSchema) - return nil, util.LoggingErrorMsg(err, errMsg) - } - } - - // if an expiry value exists, set it - if request.Expiry != "" { - if err := builder.SetExpirationDate(request.Expiry); err != nil { - errMsg := fmt.Sprintf("could not set expirty for credential: %s", request.Expiry) - return nil, util.LoggingErrorMsg(err, errMsg) - } - } - - if err := builder.SetIssuanceDate(time.Now().Format(time.RFC3339)); err != nil { - errMsg := fmt.Sprintf("could not set credential issuance date") - return nil, util.LoggingErrorMsg(err, errMsg) - } - - cred, err := builder.Build() - if err != nil { - errMsg := "could not build credential" - return nil, util.LoggingErrorMsg(err, errMsg) - } - - // store the credential - storageRequest := credstorage.StoredCredential{ - ID: cred.ID, - Credential: *cred, - Issuer: request.Issuer, - Subject: request.Subject, - Schema: request.JSONSchema, - IssuanceDate: cred.IssuanceDate, - } - if err := s.storage.StoreCredential(storageRequest); err != nil { - errMsg := "could not store credential" - return nil, util.LoggingErrorMsg(err, errMsg) - } - - // return the result - response := CreateCredentialResponse{Credential: *cred} - return &response, nil + logrus.Debugf("creating credential: %+v", request) + + builder := credential.NewVerifiableCredentialBuilder() + + if err := builder.SetIssuer(request.Issuer); err != nil { + errMsg := fmt.Sprintf("could not build credential when setting issuer: %s", request.Issuer) + return nil, util.LoggingErrorMsg(err, errMsg) + } + + // check if there's a conflict with subject ID + if id, ok := request.Data[credential.VerifiableCredentialIDProperty]; ok && id != request.Subject { + errMsg := fmt.Sprintf("cannot set subject<%s>, data already contains a different ID value: %s", request.Subject, id) + logrus.Error(errMsg) + return nil, util.LoggingNewError(errMsg) + } + + // set subject value + subject := credential.CredentialSubject(request.Data) + subject[credential.VerifiableCredentialIDProperty] = request.Subject + + if err := builder.SetCredentialSubject(subject); err != nil { + errMsg := fmt.Sprintf("could not set subject: %+v", subject) + return nil, util.LoggingErrorMsg(err, errMsg) + } + + // if a context value exists, set it + if request.Context != "" { + if err := builder.AddContext(request.Context); err != nil { + errMsg := fmt.Sprintf("could not add context to credential: %s", request.Context) + return nil, util.LoggingErrorMsg(err, errMsg) + } + } + + // if a schema value exists, set it + if request.JSONSchema != "" { + schema := credential.CredentialSchema{ + ID: request.JSONSchema, + Type: SchemaType, + } + if err := builder.SetCredentialSchema(schema); err != nil { + errMsg := fmt.Sprintf("could not set JSON Schema for credential: %s", request.JSONSchema) + return nil, util.LoggingErrorMsg(err, errMsg) + } + } + + // if an expiry value exists, set it + if request.Expiry != "" { + if err := builder.SetExpirationDate(request.Expiry); err != nil { + errMsg := fmt.Sprintf("could not set expirty for credential: %s", request.Expiry) + return nil, util.LoggingErrorMsg(err, errMsg) + } + } + + if err := builder.SetIssuanceDate(time.Now().Format(time.RFC3339)); err != nil { + errMsg := fmt.Sprintf("could not set credential issuance date") + return nil, util.LoggingErrorMsg(err, errMsg) + } + + cred, err := builder.Build() + if err != nil { + errMsg := "could not build credential" + return nil, util.LoggingErrorMsg(err, errMsg) + } + + // store the credential + storageRequest := credstorage.StoredCredential{ + ID: cred.ID, + Credential: *cred, + Issuer: request.Issuer, + Subject: request.Subject, + Schema: request.JSONSchema, + IssuanceDate: cred.IssuanceDate, + } + if err := s.storage.StoreCredential(storageRequest); err != nil { + errMsg := "could not store credential" + return nil, util.LoggingErrorMsg(err, errMsg) + } + + // return the result + response := CreateCredentialResponse{Credential: *cred} + return &response, nil } func (s Service) GetCredential(request GetCredentialRequest) (*GetCredentialResponse, error) { - logrus.Debugf("getting credential: %s", request.ID) + logrus.Debugf("getting credential: %s", request.ID) - gotCred, err := s.storage.GetCredential(request.ID) - if err != nil { - errMsg := fmt.Sprintf("could not get credential: %s", request.ID) - return nil, util.LoggingErrorMsg(err, errMsg) - } + gotCred, err := s.storage.GetCredential(request.ID) + if err != nil { + errMsg := fmt.Sprintf("could not get credential: %s", request.ID) + return nil, util.LoggingErrorMsg(err, errMsg) + } - response := GetCredentialResponse{Credential: gotCred.Credential} - return &response, nil + response := GetCredentialResponse{Credential: gotCred.Credential} + return &response, nil } func (s Service) GetCredentialsByIssuer(request GetCredentialByIssuerRequest) (*GetCredentialsResponse, error) { - logrus.Debugf("getting credential(s) for issuer: %s", util.SanitizingLog(request.Issuer)) + logrus.Debugf("getting credential(s) for issuer: %s", util.SanitizeLog(request.Issuer)) - gotCreds, err := s.storage.GetCredentialsByIssuer(request.Issuer) - if err != nil { - errMsg := fmt.Sprintf("could not get credential(s) for issuer: %s", request.Issuer) - return nil, util.LoggingErrorMsg(err, errMsg) - } + gotCreds, err := s.storage.GetCredentialsByIssuer(request.Issuer) + if err != nil { + errMsg := fmt.Sprintf("could not get credential(s) for issuer: %s", request.Issuer) + return nil, util.LoggingErrorMsg(err, errMsg) + } - var creds []credential.VerifiableCredential - for _, cred := range gotCreds { - creds = append(creds, cred.Credential) - } + var creds []credential.VerifiableCredential + for _, cred := range gotCreds { + creds = append(creds, cred.Credential) + } - response := GetCredentialsResponse{Credentials: creds} - return &response, nil + response := GetCredentialsResponse{Credentials: creds} + return &response, nil } func (s Service) GetCredentialsBySubject(request GetCredentialBySubjectRequest) (*GetCredentialsResponse, error) { - logrus.Debugf("getting credential(s) for subject: %s", util.SanitizingLog(request.Subject)) + logrus.Debugf("getting credential(s) for subject: %s", util.SanitizeLog(request.Subject)) - gotCreds, err := s.storage.GetCredentialsBySubject(request.Subject) - if err != nil { - errMsg := fmt.Sprintf("could not get credential(s) for subject: %s", request.Subject) - return nil, util.LoggingErrorMsg(err, errMsg) - } + gotCreds, err := s.storage.GetCredentialsBySubject(request.Subject) + if err != nil { + errMsg := fmt.Sprintf("could not get credential(s) for subject: %s", request.Subject) + return nil, util.LoggingErrorMsg(err, errMsg) + } - var creds []credential.VerifiableCredential - for _, cred := range gotCreds { - creds = append(creds, cred.Credential) - } + var creds []credential.VerifiableCredential + for _, cred := range gotCreds { + creds = append(creds, cred.Credential) + } - response := GetCredentialsResponse{Credentials: creds} - return &response, nil + response := GetCredentialsResponse{Credentials: creds} + return &response, nil } func (s Service) GetCredentialsBySchema(request GetCredentialBySchemaRequest) (*GetCredentialsResponse, error) { - logrus.Debugf("getting credential(s) for schema: %s", util.SanitizingLog(request.Schema)) + logrus.Debugf("getting credential(s) for schema: %s", util.SanitizeLog(request.Schema)) - gotCreds, err := s.storage.GetCredentialsBySchema(request.Schema) - if err != nil { - errMsg := fmt.Sprintf("could not get credential(s) for schema: %s", request.Schema) - return nil, util.LoggingErrorMsg(err, errMsg) - } + gotCreds, err := s.storage.GetCredentialsBySchema(request.Schema) + if err != nil { + errMsg := fmt.Sprintf("could not get credential(s) for schema: %s", request.Schema) + return nil, util.LoggingErrorMsg(err, errMsg) + } - var creds []credential.VerifiableCredential - for _, cred := range gotCreds { - creds = append(creds, cred.Credential) - } + var creds []credential.VerifiableCredential + for _, cred := range gotCreds { + creds = append(creds, cred.Credential) + } - response := GetCredentialsResponse{Credentials: creds} - return &response, nil + response := GetCredentialsResponse{Credentials: creds} + return &response, nil } func (s Service) DeleteCredential(request DeleteCredentialRequest) error { - logrus.Debugf("deleting credential: %s", request.ID) + logrus.Debugf("deleting credential: %s", request.ID) - if err := s.storage.DeleteCredential(request.ID); err != nil { - errMsg := fmt.Sprintf("could not delete credential with id: %s", request.ID) - return util.LoggingErrorMsg(err, errMsg) - } + if err := s.storage.DeleteCredential(request.ID); err != nil { + errMsg := fmt.Sprintf("could not delete credential with id: %s", request.ID) + return util.LoggingErrorMsg(err, errMsg) + } - return nil + return nil } diff --git a/pkg/service/credential/storage/bolt.go b/pkg/service/credential/storage/bolt.go index 211436621..3076dd15b 100644 --- a/pkg/service/credential/storage/bolt.go +++ b/pkg/service/credential/storage/bolt.go @@ -7,71 +7,72 @@ import ( "github.com/goccy/go-json" "github.com/pkg/errors" "github.com/sirupsen/logrus" + "github.com/tbd54566975/ssi-service/internal/util" "github.com/tbd54566975/ssi-service/pkg/storage" ) const ( - namespace = "credential" - credentialNotFoundErrMsg = "credential not found" + namespace = "credential" + credentialNotFoundErrMsg = "credential not found" ) type BoltCredentialStorage struct { - db *storage.BoltDB + db *storage.BoltDB } func NewBoltCredentialStorage(db *storage.BoltDB) (*BoltCredentialStorage, error) { - if db == nil { - return nil, errors.New("bolt db reference is nil") - } - return &BoltCredentialStorage{db: db}, nil + if db == nil { + return nil, errors.New("bolt db reference is nil") + } + return &BoltCredentialStorage{db: db}, nil } func (b BoltCredentialStorage) StoreCredential(credential StoredCredential) error { - id := credential.Credential.ID - if id == "" { - return util.LoggingNewError("could not store credential without an ID") - } - - // create and set prefix key for the credential - credential.ID = createPrefixKey(id, credential.Issuer, credential.Subject, credential.Schema) - - credBytes, err := json.Marshal(credential) - if err != nil { - errMsg := fmt.Sprintf("could not store credential: %s", id) - return util.LoggingErrorMsg(err, errMsg) - } - return b.db.Write(namespace, credential.ID, credBytes) + id := credential.Credential.ID + if id == "" { + return util.LoggingNewError("could not store credential without an ID") + } + + // create and set prefix key for the credential + credential.ID = createPrefixKey(id, credential.Issuer, credential.Subject, credential.Schema) + + credBytes, err := json.Marshal(credential) + if err != nil { + errMsg := fmt.Sprintf("could not store credential: %s", id) + return util.LoggingErrorMsg(err, errMsg) + } + return b.db.Write(namespace, credential.ID, credBytes) } func (b BoltCredentialStorage) GetCredential(id string) (*StoredCredential, error) { - prefixValues, err := b.db.ReadPrefix(namespace, id) - if err != nil { - errMsg := fmt.Sprintf("could not get credential from storage: %s", id) - return nil, util.LoggingErrorMsg(err, errMsg) - } - if len(prefixValues) > 1 { - err := fmt.Errorf("multiple prefix values matched credential id: %s", id) - return nil, util.LoggingErrorMsg(err, "could not get credential from storage") - } - - // since we know the map now only has a single value, we break after the first element - var credBytes []byte - for _, v := range prefixValues { - credBytes = v - break - } - if len(credBytes) == 0 { - err := fmt.Errorf("%s with id: %s", credentialNotFoundErrMsg, id) - return nil, util.LoggingErrorMsg(err, "could not get credential from storage") - } - - var stored StoredCredential - if err := json.Unmarshal(credBytes, &stored); err != nil { - errMsg := fmt.Sprintf("could not unmarshal stored credential: %s", id) - return nil, util.LoggingErrorMsg(err, errMsg) - } - return &stored, nil + prefixValues, err := b.db.ReadPrefix(namespace, id) + if err != nil { + errMsg := fmt.Sprintf("could not get credential from storage: %s", id) + return nil, util.LoggingErrorMsg(err, errMsg) + } + if len(prefixValues) > 1 { + err := fmt.Errorf("multiple prefix values matched credential id: %s", id) + return nil, util.LoggingErrorMsg(err, "could not get credential from storage") + } + + // since we know the map now only has a single value, we break after the first element + var credBytes []byte + for _, v := range prefixValues { + credBytes = v + break + } + if len(credBytes) == 0 { + err := fmt.Errorf("%s with id: %s", credentialNotFoundErrMsg, id) + return nil, util.LoggingErrorMsg(err, "could not get credential from storage") + } + + var stored StoredCredential + if err := json.Unmarshal(credBytes, &stored); err != nil { + errMsg := fmt.Sprintf("could not unmarshal stored credential: %s", id) + return nil, util.LoggingErrorMsg(err, errMsg) + } + return &stored, nil } // Note: this is a lazy implementation. Optimizations are to be had by adjusting prefix @@ -82,166 +83,166 @@ func (b BoltCredentialStorage) GetCredential(id string) (*StoredCredential, erro // 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) - if err != nil { - errMsg := fmt.Sprintf("could not read credential storage while searching for creds for issuer: %s", issuer) - return nil, util.LoggingErrorMsg(err, errMsg) - } - // see if the prefix keys contains the issuer value - var issuerKeys []string - for _, k := range keys { - if strings.Contains(k, issuer) { - issuerKeys = append(issuerKeys, k) - } - } - if len(issuerKeys) == 0 { - logrus.Warnf("no credentials found for issuer: %s", util.SanitizingLog(issuer)) - return nil, nil - } - - // now get each credential by key - var storedCreds []StoredCredential - for _, key := range issuerKeys { - 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", issuerKeys) - } - - return storedCreds, nil + keys, err := b.db.ReadAllKeys(namespace) + if err != nil { + errMsg := fmt.Sprintf("could not read credential storage while searching for creds for issuer: %s", issuer) + return nil, util.LoggingErrorMsg(err, errMsg) + } + // see if the prefix keys contains the issuer value + var issuerKeys []string + for _, k := range keys { + if strings.Contains(k, issuer) { + issuerKeys = append(issuerKeys, k) + } + } + if len(issuerKeys) == 0 { + logrus.Warnf("no credentials found for issuer: %s", util.SanitizeLog(issuer)) + return nil, nil + } + + // now get each credential by key + var storedCreds []StoredCredential + for _, key := range issuerKeys { + 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", issuerKeys) + } + + return storedCreds, nil } // GetCredentialsBySubject gets all credentials stored with a prefix key containing the subject 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) GetCredentialsBySubject(subject string) ([]StoredCredential, error) { - keys, err := b.db.ReadAllKeys(namespace) - if err != nil { - errMsg := fmt.Sprintf("could not read credential storage while searching for creds for subject: %s", subject) - return nil, util.LoggingErrorMsg(err, errMsg) - } - - // see if the prefix keys contains the subject value - var subjectKeys []string - for _, k := range keys { - if strings.Contains(k, subject) { - subjectKeys = append(subjectKeys, k) - } - } - if len(subjectKeys) == 0 { - logrus.Warnf("no credentials found for subject: %s", util.SanitizingLog(subject)) - return nil, nil - } - - // now get each credential by key - var storedCreds []StoredCredential - for _, key := range subjectKeys { - 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 subject: %s", subjectKeys) - } - - return storedCreds, nil + keys, err := b.db.ReadAllKeys(namespace) + if err != nil { + errMsg := fmt.Sprintf("could not read credential storage while searching for creds for subject: %s", subject) + return nil, util.LoggingErrorMsg(err, errMsg) + } + + // see if the prefix keys contains the subject value + var subjectKeys []string + for _, k := range keys { + if strings.Contains(k, subject) { + subjectKeys = append(subjectKeys, k) + } + } + if len(subjectKeys) == 0 { + logrus.Warnf("no credentials found for subject: %s", util.SanitizeLog(subject)) + return nil, nil + } + + // now get each credential by key + var storedCreds []StoredCredential + for _, key := range subjectKeys { + 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 subject: %s", subjectKeys) + } + + return storedCreds, nil } // GetCredentialsBySchema gets all credentials stored with a prefix key containing the schema 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) GetCredentialsBySchema(schema string) ([]StoredCredential, error) { - keys, err := b.db.ReadAllKeys(namespace) - if err != nil { - errMsg := fmt.Sprintf("could not read credential storage while searching for creds for schema: %s", schema) - return nil, util.LoggingErrorMsg(err, errMsg) - } - - // see if the prefix keys contains the schema value - query := "sc:" + schema - var schemaKeys []string - for _, k := range keys { - if strings.HasSuffix(k, query) { - schemaKeys = append(schemaKeys, k) - } - } - if len(schemaKeys) == 0 { - logrus.Warnf("no credentials found for schema: %s", util.SanitizingLog(schema)) - return nil, nil - } - - // now get each credential by key - var storedCreds []StoredCredential - for _, key := range schemaKeys { - 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 schema: %s", schemaKeys) - } - - return storedCreds, nil + keys, err := b.db.ReadAllKeys(namespace) + if err != nil { + errMsg := fmt.Sprintf("could not read credential storage while searching for creds for schema: %s", schema) + return nil, util.LoggingErrorMsg(err, errMsg) + } + + // see if the prefix keys contains the schema value + query := "sc:" + schema + var schemaKeys []string + for _, k := range keys { + if strings.HasSuffix(k, query) { + schemaKeys = append(schemaKeys, k) + } + } + if len(schemaKeys) == 0 { + logrus.Warnf("no credentials found for schema: %s", util.SanitizeLog(schema)) + return nil, nil + } + + // now get each credential by key + var storedCreds []StoredCredential + for _, key := range schemaKeys { + 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 schema: %s", schemaKeys) + } + + return storedCreds, nil } func (b BoltCredentialStorage) DeleteCredential(id string) error { - credDoesNotExistMsg := fmt.Sprintf("credential does not exist, cannot delete: %s", id) - - // first get the credential to regenerate the prefix key - gotCred, err := b.GetCredential(id) - if err != nil { - // no error on deletion for a non-existent credential - if strings.Contains(err.Error(), credentialNotFoundErrMsg) { - logrus.Warn(credDoesNotExistMsg) - return nil - } - - errMsg := fmt.Sprintf("could not get credential<%s> before deletion", id) - return util.LoggingErrorMsg(err, errMsg) - } - - // no error on deletion for a non-existent credential - if gotCred == nil { - logrus.Warn(credDoesNotExistMsg) - return nil - } - - // re-create the prefix key to delete - prefix := createPrefixKey(id, gotCred.Issuer, gotCred.Subject, gotCred.Schema) - if err := b.db.Delete(namespace, prefix); err != nil { - errMsg := fmt.Sprintf("could not delete credential: %s", id) - return util.LoggingErrorMsg(err, errMsg) - } - return nil + credDoesNotExistMsg := fmt.Sprintf("credential does not exist, cannot delete: %s", id) + + // first get the credential to regenerate the prefix key + gotCred, err := b.GetCredential(id) + if err != nil { + // no error on deletion for a non-existent credential + if strings.Contains(err.Error(), credentialNotFoundErrMsg) { + logrus.Warn(credDoesNotExistMsg) + return nil + } + + errMsg := fmt.Sprintf("could not get credential<%s> before deletion", id) + return util.LoggingErrorMsg(err, errMsg) + } + + // no error on deletion for a non-existent credential + if gotCred == nil { + logrus.Warn(credDoesNotExistMsg) + return nil + } + + // re-create the prefix key to delete + prefix := createPrefixKey(id, gotCred.Issuer, gotCred.Subject, gotCred.Schema) + if err := b.db.Delete(namespace, prefix); err != nil { + errMsg := fmt.Sprintf("could not delete credential: %s", id) + return util.LoggingErrorMsg(err, errMsg) + } + return nil } // unique key for a credential func createPrefixKey(id, issuer, subject, schema string) string { - return strings.Join([]string{id, "is:" + issuer, "su:" + subject, "sc:" + schema}, "-") + return strings.Join([]string{id, "is:" + issuer, "su:" + subject, "sc:" + schema}, "-") } diff --git a/pkg/service/credential/storage/storage.go b/pkg/service/credential/storage/storage.go index fb5958168..be26bcab9 100644 --- a/pkg/service/credential/storage/storage.go +++ b/pkg/service/credential/storage/storage.go @@ -1,7 +1,10 @@ package storage import ( + "fmt" + "github.com/TBD54566975/ssi-sdk/credential" + "github.com/tbd54566975/ssi-service/internal/util" "github.com/tbd54566975/ssi-service/pkg/storage" ) @@ -25,13 +28,20 @@ type Storage interface { } func NewCredentialStorage(s storage.ServiceStorage) (Storage, error) { - gotBolt, ok := s.(*storage.BoltDB) - if !ok { - return nil, util.LoggingNewError("unsupported storage type") - } - boltStorage, err := NewBoltCredentialStorage(gotBolt) - if err != nil { - return nil, util.LoggingErrorMsg(err, "could not instantiate credential bolt storage") + switch s.Type() { + case storage.Bolt: + gotBolt, ok := s.(*storage.BoltDB) + if !ok { + errMsg := fmt.Sprintf("trouble instantiating : %s", s.Type()) + return nil, util.LoggingNewError(errMsg) + } + boltStorage, err := NewBoltCredentialStorage(gotBolt) + if err != nil { + return nil, util.LoggingErrorMsg(err, "could not instantiate credential bolt storage") + } + return boltStorage, err + default: + errMsg := fmt.Errorf("unsupported storage type: %s", s.Type()) + return nil, util.LoggingError(errMsg) } - return boltStorage, err } diff --git a/pkg/service/did/key.go b/pkg/service/did/key.go index b09f7e90c..3d4a384e9 100644 --- a/pkg/service/did/key.go +++ b/pkg/service/did/key.go @@ -2,10 +2,13 @@ package did import ( "fmt" + "github.com/TBD54566975/ssi-sdk/did" "github.com/goccy/go-json" "github.com/mr-tron/base58" "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/tbd54566975/ssi-service/pkg/service/did/storage" ) @@ -18,6 +21,9 @@ type keyDIDHandler struct { } func (h *keyDIDHandler) CreateDID(request CreateDIDRequest) (*CreateDIDResponse, error) { + + logrus.Debugf("creating DID: %+v", request) + // create the DID privKey, doc, err := did.GenerateDIDKey(request.KeyType) if err != nil { @@ -50,6 +56,9 @@ func (h *keyDIDHandler) CreateDID(request CreateDIDRequest) (*CreateDIDResponse, } func (h *keyDIDHandler) GetDID(request GetDIDRequest) (*GetDIDResponse, error) { + + logrus.Debugf("getting DID: %+v", request) + id := request.ID gotDID, err := h.storage.GetDID(id) if err != nil { diff --git a/pkg/service/did/storage/storage.go b/pkg/service/did/storage/storage.go index c9b919536..804aeae1e 100644 --- a/pkg/service/did/storage/storage.go +++ b/pkg/service/did/storage/storage.go @@ -1,8 +1,11 @@ package storage import ( + "fmt" + "github.com/TBD54566975/ssi-sdk/did" - "github.com/pkg/errors" + + "github.com/tbd54566975/ssi-service/internal/util" "github.com/tbd54566975/ssi-service/pkg/storage" ) @@ -21,13 +24,20 @@ type Storage interface { // NewDIDStorage finds the DID storage impl for a given ServiceStorage value func NewDIDStorage(s storage.ServiceStorage) (Storage, error) { - gotBolt, ok := s.(*storage.BoltDB) - if !ok { - return nil, errors.New("unsupported storage type") - } - boltStorage, err := NewBoltDIDStorage(gotBolt) - if err != nil { - return nil, errors.Wrap(err, "could not instantiate DID Bolt storage") + switch s.Type() { + case storage.Bolt: + gotBolt, ok := s.(*storage.BoltDB) + if !ok { + errMsg := fmt.Sprintf("trouble instantiating : %s", s.Type()) + return nil, util.LoggingNewError(errMsg) + } + boltStorage, err := NewBoltDIDStorage(gotBolt) + if err != nil { + return nil, util.LoggingErrorMsg(err, "could not instantiate credential bolt storage") + } + return boltStorage, err + default: + errMsg := fmt.Errorf("unsupported storage type: %s", s.Type()) + return nil, util.LoggingError(errMsg) } - return boltStorage, err } diff --git a/pkg/service/framework/framework.go b/pkg/service/framework/framework.go index 8406f9c34..e49288a31 100644 --- a/pkg/service/framework/framework.go +++ b/pkg/service/framework/framework.go @@ -1,33 +1,34 @@ package framework type ( - Type string - StatusState string + Type string + StatusState string ) const ( - // List of all service + // List of all service - DID Type = "did" - Schema Type = "schema" - Credential Type = "credential" + DID Type = "did" + Schema Type = "schema" + Credential Type = "credential" + KeyStore Type = "keystore" - StatusReady StatusState = "ready" - StatusNotReady StatusState = "not_ready" + StatusReady StatusState = "ready" + StatusNotReady StatusState = "not_ready" ) func (t Type) String() string { - return string(t) + return string(t) } // Status is for service reporting on their status type Status struct { - Status StatusState `json:"status,omitempty"` - Message string `json:"message,omitempty"` + Status StatusState `json:"status,omitempty"` + Message string `json:"message,omitempty"` } // Service is an interface each service must comply with to be registered and orchestrated by the http. type Service interface { - Type() Type - Status() Status + Type() Type + Status() Status } diff --git a/pkg/service/keystore/keystore.go b/pkg/service/keystore/keystore.go new file mode 100644 index 000000000..9b7f6c474 --- /dev/null +++ b/pkg/service/keystore/keystore.go @@ -0,0 +1,146 @@ +package keystore + +import ( + "fmt" + "time" + + "github.com/TBD54566975/ssi-sdk/crypto" + "github.com/mr-tron/base58" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "golang.org/x/crypto/chacha20poly1305" + + "github.com/tbd54566975/ssi-service/config" + "github.com/tbd54566975/ssi-service/internal/util" + "github.com/tbd54566975/ssi-service/pkg/service/framework" + keystorestorage "github.com/tbd54566975/ssi-service/pkg/service/keystore/storage" + "github.com/tbd54566975/ssi-service/pkg/storage" +) + +type Service struct { + storage keystorestorage.Storage + config config.KeyStoreServiceConfig +} + +func (s Service) Type() framework.Type { + return framework.KeyStore +} + +func (s Service) Status() framework.Status { + if s.storage == nil { + return framework.Status{ + Status: framework.StatusNotReady, + Message: "no storage", + } + } + return framework.Status{Status: framework.StatusReady} +} + +func (s Service) Config() config.KeyStoreServiceConfig { + return s.config +} + +func NewKeyStoreService(config config.KeyStoreServiceConfig, s storage.ServiceStorage) (*Service, error) { + // First, generate a service key + serviceKey, serviceKeySalt, err := GenerateServiceKey(config.ServiceKeyPassword) + if err != nil { + return nil, util.LoggingErrorMsg(err, "could not generate service key") + } + + // Next, instantiate the key storage + keyStoreStorage, err := keystorestorage.NewKeyStoreStorage(s, serviceKey, serviceKeySalt) + if err != nil { + errMsg := "could not instantiate storage for the keystore service" + return nil, util.LoggingErrorMsg(err, errMsg) + } + + return &Service{ + storage: keyStoreStorage, + config: config, + }, nil +} + +func (s Service) StoreKey(request StoreKeyRequest) error { + + logrus.Debugf("storing key: %+v", request) + + // check if the provided key type is supported. support entails being able to serialize/deserialize, in addition + // to facilitating signing/verification and encryption/decryption support. + if !crypto.IsSupportedKeyType(request.Type) { + errMsg := fmt.Sprintf("unsupported key type: %s", request.Type) + return util.LoggingNewError(errMsg) + } + + key := keystorestorage.StoredKey{ + ID: request.ID, + Controller: request.Controller, + KeyType: request.Type, + Key: request.Key, + CreatedAt: time.Now().Format(time.RFC3339), + } + if err := s.storage.StoreKey(key); err != nil { + err := errors.Wrapf(err, "could not store key: %s", request.ID) + return util.LoggingError(err) + } + return nil +} + +func (s Service) GetKeyDetails(request GetKeyDetailsRequest) (*GetKeyDetailsResponse, error) { + + logrus.Debugf("getting key: %+v", request) + + id := request.ID + gotKeyDetails, err := s.storage.GetKeyDetails(id) + if err != nil { + err := errors.Wrapf(err, "could not get key details for key: %s", id) + return nil, util.LoggingError(err) + } + if gotKeyDetails == nil { + err := errors.Wrapf(err, "key with id<%s> could not be found", id) + return nil, util.LoggingError(err) + } + return &GetKeyDetailsResponse{ + ID: gotKeyDetails.ID, + Type: gotKeyDetails.KeyType, + Controller: gotKeyDetails.Controller, + CreatedAt: gotKeyDetails.CreatedAt, + }, nil +} + +// GenerateServiceKey using argon2 for key derivation generate a service key and corresponding salt, +// base58 encoding both values. +func GenerateServiceKey(skPassword string) (key, salt string, err error) { + saltBytes, err := util.GenerateSalt(util.Argon2SaltSize) + if err != nil { + err := errors.Wrap(err, "could not generate salt for service key") + return "", "", util.LoggingError(err) + } + + keyBytes, err := util.Argon2KeyGen(skPassword, saltBytes, chacha20poly1305.KeySize) + if err != nil { + err := errors.Wrap(err, "could not generate key for service key") + return "", "", util.LoggingError(err) + } + + key = base58.Encode(keyBytes) + salt = base58.Encode(saltBytes) + return +} + +// EncryptKey encrypts another key with the service key using xchacha20-poly1305 +func EncryptKey(serviceKey, key []byte) ([]byte, error) { + encryptedKey, err := util.XChaCha20Poly1305Encrypt(serviceKey, key) + if err != nil { + return nil, errors.Wrap(err, "could not encrypt key with service key") + } + return encryptedKey, nil +} + +// DecryptKey encrypts another key with the service key using xchacha20-poly1305 +func DecryptKey(serviceKey, encryptedKey []byte) ([]byte, error) { + decryptedKey, err := util.XChaCha20Poly1305Decrypt(serviceKey, encryptedKey) + if err != nil { + return nil, errors.Wrap(err, "could not decrypt key with service key") + } + return decryptedKey, nil +} diff --git a/pkg/service/keystore/keystore_test.go b/pkg/service/keystore/keystore_test.go new file mode 100644 index 000000000..1fad6a679 --- /dev/null +++ b/pkg/service/keystore/keystore_test.go @@ -0,0 +1,86 @@ +package keystore + +import ( + "testing" + + "github.com/TBD54566975/ssi-sdk/crypto" + "github.com/mr-tron/base58" + "github.com/stretchr/testify/assert" +) + +func TestGenerateServiceKey(t *testing.T) { + emptySKPassword := "" + _, _, err := GenerateServiceKey(emptySKPassword) + assert.Error(t, err) + + skPassword := "test-password" + key, salt, err := GenerateServiceKey(skPassword) + assert.NoError(t, err) + assert.NotEmpty(t, key) + assert.NotEmpty(t, salt) +} + +func TestEncryptDecryptAllKeyTypes(t *testing.T) { + skPassword := "test-password" + serviceKeyEncoded, _, err := GenerateServiceKey(skPassword) + assert.NoError(t, err) + serviceKey, err := base58.Decode(serviceKeyEncoded) + assert.NoError(t, err) + assert.NotEmpty(t, serviceKey) + + tests := []struct { + kt crypto.KeyType + }{ + { + kt: crypto.Ed25519, + }, + { + kt: crypto.X25519, + }, + { + kt: crypto.Secp256k1, + }, + { + kt: crypto.P224, + }, + { + kt: crypto.P256, + }, + { + kt: crypto.P384, + }, + { + kt: crypto.P521, + }, + { + kt: crypto.RSA, + }, + } + for _, test := range tests { + t.Run(string(test.kt), func(t *testing.T) { + // generate a new key based on the given key type + _, privKey, err := crypto.GenerateKeyByKeyType(test.kt) + assert.NoError(t, err) + assert.NotEmpty(t, privKey) + + // serialize the key before encryption + privKeyBytes, err := crypto.PrivKeyToBytes(privKey) + assert.NoError(t, err) + assert.NotEmpty(t, privKeyBytes) + + // encrypt the serviceKey using our service serviceKey + encryptedKey, err := EncryptKey(serviceKey, privKeyBytes) + assert.NoError(t, err) + assert.NotEmpty(t, encryptedKey) + + // decrypt the serviceKey using our service serviceKey + decryptedKey, err := DecryptKey(serviceKey, encryptedKey) + assert.NoError(t, err) + assert.NotEmpty(t, decryptedKey) + + // reconstruct the key from its serialized form + privKeyReconstructed, err := crypto.BytesToPrivKey(decryptedKey, test.kt) + assert.EqualValues(t, privKey, privKeyReconstructed) + }) + } +} diff --git a/pkg/service/keystore/model.go b/pkg/service/keystore/model.go new file mode 100644 index 000000000..2f4afac00 --- /dev/null +++ b/pkg/service/keystore/model.go @@ -0,0 +1,23 @@ +package keystore + +import ( + "github.com/TBD54566975/ssi-sdk/crypto" +) + +type StoreKeyRequest struct { + ID string + Type crypto.KeyType + Controller string + Key []byte +} + +type GetKeyDetailsRequest struct { + ID string +} + +type GetKeyDetailsResponse struct { + ID string + Type crypto.KeyType + Controller string + CreatedAt string +} diff --git a/pkg/service/keystore/storage/bolt.go b/pkg/service/keystore/storage/bolt.go new file mode 100644 index 000000000..dd4e61ca0 --- /dev/null +++ b/pkg/service/keystore/storage/bolt.go @@ -0,0 +1,96 @@ +package storage + +import ( + "fmt" + + "github.com/goccy/go-json" + "github.com/pkg/errors" + + "github.com/tbd54566975/ssi-service/internal/util" + "github.com/tbd54566975/ssi-service/pkg/storage" +) + +const ( + namespace = "keystore" + skKey = "ssi-service-key" + keyNotFoundErrMsg = "key not found" +) + +type BoltKeyStoreStorage struct { + db *storage.BoltDB +} + +func NewBoltKeyStoreStorage(db *storage.BoltDB, key ServiceKey) (*BoltKeyStoreStorage, error) { + if db == nil { + return nil, errors.New("bolt db reference is nil") + } + + bolt := &BoltKeyStoreStorage{db: db} + + // first, store the service key + if err := bolt.storeServiceKey(key); err != nil { + return nil, errors.Wrap(err, "could not store service key") + } + return bolt, nil +} + +// TODO(gabe): support more robust service key operations, including rotation, and caching +func (b BoltKeyStoreStorage) storeServiceKey(key ServiceKey) error { + keyBytes, err := json.Marshal(key) + if err != nil { + return util.LoggingErrorMsg(err, "could not marshal service key") + } + if err := b.db.Write(namespace, skKey, keyBytes); err != nil { + return util.LoggingErrorMsg(err, "could store marshal service key") + } + return nil +} + +func (b BoltKeyStoreStorage) getServiceKey() (*ServiceKey, error) { + skBytes, err := b.db.Read(namespace, skKey) + if err != nil { + return nil, util.LoggingErrorMsg(err, "could not retrieve service key") + } + var serviceKey ServiceKey + if err := json.Unmarshal(skBytes, &serviceKey); err != nil { + return nil, util.LoggingErrorMsg(err, "could not unmarshal service key") + } + return &serviceKey, nil +} + +func (b BoltKeyStoreStorage) StoreKey(key StoredKey) error { + id := key.ID + if id == "" { + return util.LoggingNewError("could not store key without an ID") + } + + keyBytes, err := json.Marshal(key) + if err != nil { + errMsg := fmt.Sprintf("could not store key: %s", id) + return util.LoggingErrorMsg(err, errMsg) + } + return b.db.Write(namespace, id, keyBytes) +} + +func (b BoltKeyStoreStorage) GetKeyDetails(id string) (*KeyDetails, error) { + storedKeyBytes, err := b.db.Read(namespace, id) + if err != nil { + errMsg := fmt.Sprintf("could not get key details for key: %s", id) + return nil, util.LoggingErrorMsg(err, errMsg) + } + if len(storedKeyBytes) == 0 { + err := fmt.Errorf("could not find key details for key: %s", id) + return nil, util.LoggingError(err) + } + var stored StoredKey + if err := json.Unmarshal(storedKeyBytes, &stored); err != nil { + errMsg := fmt.Sprintf("could not unmarshal stored key: %s", id) + return nil, util.LoggingErrorMsg(err, errMsg) + } + return &KeyDetails{ + ID: stored.ID, + Controller: stored.Controller, + KeyType: stored.KeyType, + CreatedAt: stored.CreatedAt, + }, nil +} diff --git a/pkg/service/keystore/storage/storage.go b/pkg/service/keystore/storage/storage.go new file mode 100644 index 000000000..67799c13c --- /dev/null +++ b/pkg/service/keystore/storage/storage.go @@ -0,0 +1,59 @@ +package storage + +import ( + "fmt" + + "github.com/TBD54566975/ssi-sdk/crypto" + + "github.com/tbd54566975/ssi-service/internal/util" + "github.com/tbd54566975/ssi-service/pkg/storage" +) + +// StoredKey represents a common data model to store data on all key types +type StoredKey struct { + ID string `json:"id"` + Controller string `json:"controller"` + KeyType crypto.KeyType `json:"keyType"` + Key []byte `json:"key"` + CreatedAt string `json:"createdAt"` +} + +// KeyDetails represents a common data model to get information about a key, without revealing the key itself +type KeyDetails struct { + ID string `json:"id"` + Controller string `json:"controller"` + KeyType crypto.KeyType `json:"keyType"` + CreatedAt string `json:"createdAt"` +} + +type ServiceKey struct { + Key string + Salt string +} + +type Storage interface { + StoreKey(key StoredKey) error + GetKeyDetails(id string) (*KeyDetails, error) +} + +func NewKeyStoreStorage(s storage.ServiceStorage, serviceKey, serviceKeySalt string) (Storage, error) { + switch s.Type() { + case storage.Bolt: + gotBolt, ok := s.(*storage.BoltDB) + if !ok { + errMsg := fmt.Sprintf("trouble instantiating : %s", s.Type()) + return nil, util.LoggingNewError(errMsg) + } + boltStorage, err := NewBoltKeyStoreStorage(gotBolt, ServiceKey{ + Key: serviceKey, + Salt: serviceKeySalt, + }) + if err != nil { + return nil, util.LoggingErrorMsg(err, "could not instantiate key store bolt storage") + } + return boltStorage, err + default: + errMsg := fmt.Errorf("unsupported storage type: %s", s.Type()) + return nil, util.LoggingError(errMsg) + } +} diff --git a/pkg/service/schema/storage/storage.go b/pkg/service/schema/storage/storage.go index 1bdc694b8..4379c7933 100644 --- a/pkg/service/schema/storage/storage.go +++ b/pkg/service/schema/storage/storage.go @@ -1,8 +1,11 @@ package storage import ( + "fmt" + "github.com/TBD54566975/ssi-sdk/credential/schema" - "github.com/pkg/errors" + + "github.com/tbd54566975/ssi-service/internal/util" "github.com/tbd54566975/ssi-service/pkg/storage" ) @@ -20,13 +23,20 @@ type Storage interface { // NewSchemaStorage finds the schema storage impl for a given ServiceStorage value func NewSchemaStorage(s storage.ServiceStorage) (Storage, error) { - gotBolt, ok := s.(*storage.BoltDB) - if !ok { - return nil, errors.New("unsupported storage type") - } - boltStorage, err := NewBoltSchemaStorage(gotBolt) - if err != nil { - return nil, errors.Wrap(err, "could not instantiate schema bolt storage") + switch s.Type() { + case storage.Bolt: + gotBolt, ok := s.(*storage.BoltDB) + if !ok { + errMsg := fmt.Sprintf("trouble instantiating : %s", s.Type()) + return nil, util.LoggingNewError(errMsg) + } + boltStorage, err := NewBoltSchemaStorage(gotBolt) + if err != nil { + return nil, util.LoggingErrorMsg(err, "could not instantiate schema bolt storage") + } + return boltStorage, err + default: + errMsg := fmt.Errorf("unsupported storage type: %s", s.Type()) + return nil, util.LoggingError(errMsg) } - return boltStorage, err } diff --git a/pkg/service/service.go b/pkg/service/service.go index bf6f01460..c71e15e2a 100644 --- a/pkg/service/service.go +++ b/pkg/service/service.go @@ -2,6 +2,7 @@ package service import ( "fmt" + "github.com/tbd54566975/ssi-service/config" "github.com/tbd54566975/ssi-service/internal/util" "github.com/tbd54566975/ssi-service/pkg/service/credential" @@ -26,7 +27,7 @@ func InstantiateSSIService(config config.ServicesConfig) (*SSIService, error) { } services, err := instantiateServices(config) if err != nil { - errMsg := "could not instantiate the verifiable credentials service" + errMsg := "could not instantiate the ssi service" return nil, util.LoggingErrorMsg(err, errMsg) } return &SSIService{services: services}, nil diff --git a/pkg/storage/bolt.go b/pkg/storage/bolt.go index 3cf9c587c..c8add6d41 100644 --- a/pkg/storage/bolt.go +++ b/pkg/storage/bolt.go @@ -3,11 +3,12 @@ package storage import ( "bytes" "fmt" + "strings" + "time" + "github.com/boltdb/bolt" "github.com/pkg/errors" "github.com/sirupsen/logrus" - "strings" - "time" ) const ( @@ -18,6 +19,10 @@ type BoltDB struct { db *bolt.DB } +func (b *BoltDB) Type() Storage { + return Bolt +} + // NewBoltDB instantiates a file-based storage instance for Bolt https://github.com/boltdb/bolt func NewBoltDB() (*BoltDB, error) { return NewBoltDBWithFile(DBFile) diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go index 59f17b29c..860872d77 100644 --- a/pkg/storage/storage.go +++ b/pkg/storage/storage.go @@ -12,6 +12,7 @@ const ( // ServiceStorage describes the api for storage independent of DB providers type ServiceStorage interface { + Type() Storage Close() error Write(namespace, key string, value []byte) error Read(namespace, key string) ([]byte, error)