Repository: TBD54566975/ssi-service
Pull request #62 (2022-07-18 19:26:04)

Add a KeyStore Service
- Light way to store keys
- Retrieve details about said keys
- No more functionality yet (signing, verification, access controls)


Addresses https://github.com/TBD54566975/ssi-service/issues/40


Patch:
diff --git a/config/config.go b/config/config.go
index 6730208..18f7e56 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 98e818d..6138153 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 d8945d3..b0bd68f 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 caebe77..e4211c0 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 0000000..9ecdc21
--- /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/util.go b/internal/util/util.go
index f24f2dd..bf7ae6c 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 01db0c7..1fb5e45 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 53cfae5..d739cf1 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/keystore.go b/pkg/server/router/keystore.go
new file mode 100644
index 0000000..c7b9b0a
--- /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/server.go b/pkg/server/server.go
index 1308de8..9120edd 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/service/credential/credential.go b/pkg/service/credential/credential.go
index faf271e..fe95e27 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 2114366..3076dd1 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 fb59581..be26bca 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 b09f7e9..3d4a384 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 c9b9195..804aeae 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 8406f9c..e49288a 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 0000000..9b7f6c4
--- /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/model.go b/pkg/service/keystore/model.go
new file mode 100644
index 0000000..2f4afac
--- /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 0000000..dd4e61c
--- /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 0000000..67799c1
--- /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 1bdc694..4379c79 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 bf6f014..c71e15e 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 3cf9c58..c8add6d 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 59f17b2..860872d 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)


Test patch:
diff --git a/internal/util/crypto_test.go b/internal/util/crypto_test.go
new file mode 100644
index 0000000..9242cc6
--- /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/pkg/server/router/did_test.go b/pkg/server/router/did_test.go
index 7c5a0e7..248bd02 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<bad>")
-
-		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<bad>")
+
+        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_test.go b/pkg/server/router/keystore_test.go
new file mode 100644
index 0000000..a5bb87e
--- /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 0000000..5d29aaa
--- /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_test.go b/pkg/server/server_test.go
index af0058e..2966d89 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<bad>")
 
@@ -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/keystore/keystore_test.go b/pkg/service/keystore/keystore_test.go
new file mode 100644
index 0000000..1fad6a6
--- /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)
+		})
+	}
+}