Repository: TBD54566975/ssi-service
Pull request #35 (2022-05-03 03:18:05)

File driven config
Fixes https://github.com/TBD54566975/ssi-service/issues/33


Patch:
diff --git a/README.md b/README.md
index 63460fd..9b0b95b 100644
--- a/README.md
+++ b/README.md
@@ -9,22 +9,45 @@
 ## Introduction
 
 The Self Sovereign Identity Service (SSIS) facilitates all things relating to [DIDs](https://www.w3.org/TR/did-core/)
-and [Verifiable Credentials](https://www.w3.org/TR/vc-data-model) -- in a box! The service is a part of a larger 
-Decentralized Web Platform architecture which you can learn more about in our 
+and [Verifiable Credentials](https://www.w3.org/TR/vc-data-model) -- in a box! The service is a part of a larger
+Decentralized Web Platform architecture which you can learn more about in our
 [collaboration repo](https://github.com/TBD54566975/collaboration). The SSI Service is a RESTful web service that
-wraps the [ssi-sdk](https://github.com/TBD54566975/ssi-sdk). The core functionality of the SSIS includes, 
+wraps the [ssi-sdk](https://github.com/TBD54566975/ssi-sdk). The core functionality of the SSIS includes,
 but is not limited to: interacting with the standards around Verifiable Credentials, Credential Revocations, requesting
-Credentials, exchanging Credentials, data schemas for Credentials and other verifiable data, messaging using 
+Credentials, exchanging Credentials, data schemas for Credentials and other verifiable data, messaging using
 Decentralized Web Nodes, and usage of Decentralized Identifiers. Using these core standards, the SSIS enables robust
-functionality to facilitate all verifiable interactions such as creating, signing, issuing, curating, requesting, 
+functionality to facilitate all verifiable interactions such as creating, signing, issuing, curating, requesting,
 revoking, exchanging, validating, verifying credentials in varying degrees of complexity.
 
 ![ssi-sdk](doc/ssi-service.png)
 
+## Configuration
+
+Configuration is managed using a [TOML](https://toml.io/en/) [file](config/config.toml). There are sets of configuration
+values for the server (e.g. which port to listen on), the services (e.g. which database to use, and each service. Each 
+service may define specific configuration, such as which DID methods are enabled for the DID service.
+
+## Build & Test
+
+This project uses [mage](https://magefile.org/), please view [CONTRIBUTING](CONTRIBUTING.md) for more information.
+
+After installing mage, you can build and test the SDK with the following commands:
+```
+mage build
+mage test
+```
+
+A utility is provided to run _clean, build, and test_ in sequence with:
+
+```
+mage cbt
+```
+
 ## Continuous Integration
 
 CI is managed via [GitHub Actions](https://github.com/TBD54566975/ssi-service/actions). Actions are triggered to run
-for each Pull Request, and on merge to `main`. You can run CI locally using a tool like [act](https://github.com/nektos/act).
+for each Pull Request, and on merge to `main`. You can run CI locally using a tool
+like [act](https://github.com/nektos/act).
 
 ## Deployment
 
@@ -43,6 +66,7 @@ cd build && docker-compose up
 ```
 
 You should then be able to send requests as follows:
+
 ```shell
  ~ curl localhost:8080/health
 {"status":"OK"}
@@ -51,10 +75,11 @@ You should then be able to send requests as follows:
 ## What's Supported?
 
 - [x] [DID Management](https://www.w3.org/TR/did-core/)
-  - Using [did:key](https://w3c-ccg.github.io/did-method-key/)
-- [ ] [Verifiable Credential Schema](https://w3c-ccg.github.io/vc-json-schemas/v2/index.html) Management
-- [x] [Verifiable Credential](https://www.w3.org/TR/vc-data-model) Issuance & Verification
-- [ ] Requesting, Receiving, and the Validation of Verifiable Claims using [Presentation Exchange](https://identity.foundation/presentation-exchange/)
+    - Using [did:key](https://w3c-ccg.github.io/did-method-key/)
+- [x] [Verifiable Credential Schema](https://w3c-ccg.github.io/vc-json-schemas/v2/index.html) Management
+- [] [Verifiable Credential](https://www.w3.org/TR/vc-data-model) Issuance & Verification
+- [ ] Requesting, Receiving, and the Validation of Verifiable Claims
+  using [Presentation Exchange](https://identity.foundation/presentation-exchange/)
 - [ ] Applying for Verifiable Credentials using [Credential Manifest](https://identity.foundation/credential-manifest/)
 - [ ] Revocations of Verifiable Credentials using the [Status List 2021](https://w3c-ccg.github.io/vc-status-list-2021/)
 - [ ] [ID Hub](https://identity.foundation/identity-hub/spec/) Messaging
@@ -80,4 +105,5 @@ This project is fully open source, and we welcome contributions! For more inform
 | [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md) | Expected behavior for project contributors, promoting a welcoming environment |
 | [CONTRIBUTING.md](./CONTRIBUTING.md)       | Developer guide to build, test, run, access CI, chat, discuss, file issues    |
 | [GOVERNANCE.md](./GOVERNANCE.md)           | Project governance                                                            |
+| [SECURITY.md](./SECRUTIY.md)               | Vulnerability and bug reporting                                               |
 | [LICENSE](./LICENSE)                       | Apache License, Version 2.0                                                   |
diff --git a/cmd/main.go b/cmd/main.go
index 1ed9403..712161c 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -3,23 +3,19 @@ package main
 import (
 	"context"
 	"expvar"
-	"fmt"
+	"github.com/ardanlabs/conf"
+	"github.com/pkg/errors"
+	"github.com/tbd54566975/ssi-service/config"
 	"github.com/tbd54566975/ssi-service/pkg/server"
-	"github.com/tbd54566975/ssi-service/pkg/service"
 	"log"
 	"net/http"
 	"os"
 	"os/signal"
 	"syscall"
-	"time"
-
-	"github.com/ardanlabs/conf"
-	"github.com/pkg/errors"
 )
 
 const (
-	ServiceName = "ssi-server"
-	LogPrefix   = ServiceName + ": "
+	LogPrefix = config.ServiceName + ": "
 )
 
 func main() {
@@ -32,64 +28,24 @@ func main() {
 	}
 }
 
-type serverConfig struct {
-	conf.Version
-	Web *webConfig
-}
-
-type webConfig struct {
-	APIHost         string        `conf:"default:0.0.0.0:3000"`
-	DebugHost       string        `conf:"default:0.0.0.0:4000"`
-	ReadTimeout     time.Duration `conf:"default:5s"`
-	WriteTimeout    time.Duration `conf:"default:5s"`
-	ShutdownTimeout time.Duration `conf:"default:5s"`
-}
-
 // startup and shutdown logic
-func run(log *log.Logger) error {
-	cfg := serverConfig{
-		Version: conf.Version{
-			SVN:  "2022.03.15",
-			Desc: "The Self Sovereign Identity Service",
-		},
-		Web: new(webConfig),
-	}
-
-	if err := conf.Parse(os.Args[1:], ServiceName, &cfg); err != nil {
-		switch err {
-		case conf.ErrHelpWanted:
-			usage, err := conf.Usage(ServiceName, &cfg)
-			if err != nil {
-				return errors.Wrap(err, "parsing config")
-			}
-			fmt.Println(usage)
-
-			return nil
-
-		case conf.ErrVersionWanted:
-			version, err := conf.VersionString(ServiceName, &cfg)
-			if err != nil {
-				return errors.Wrap(err, "generating cfg version")
-			}
-
-			fmt.Println(version)
-			return nil
-		}
-
-		return errors.Wrap(err, "parsing config")
+func run(logger *log.Logger) error {
+	cfg, err := config.LoadConfig(logger, config.DefaultConfigPath)
+	if err != nil {
+		logger.Fatalf("could not instantiate config: %s", err.Error())
 	}
 
 	expvar.NewString("build").Set(cfg.Version.SVN)
 
-	log.Printf("main: Started : Service initializing : version %q", cfg.Version.SVN)
-	defer log.Println("main: Completed")
+	logger.Printf("main: Started : Service initializing : version %q", cfg.Version.SVN)
+	defer logger.Println("main: Completed")
 
 	out, err := conf.String(&cfg)
 	if err != nil {
 		return errors.Wrap(err, "serializing config")
 	}
 
-	log.Printf("main: Config: \n%v\n", out)
+	logger.Printf("main: Config: \n%v\n", out)
 
 	// create a channel of buffer size 1 to handle shutdown.
 	// buffer's size is 1 in order to ignore any additional ctrl+c
@@ -97,22 +53,21 @@ func run(log *log.Logger) error {
 	shutdown := make(chan os.Signal, 1)
 	signal.Notify(shutdown, os.Interrupt, syscall.SIGTERM)
 
-	serviceConfig := service.Config{Logger: log}
-	ssiServer, err := server.NewSSIServer(shutdown, serviceConfig)
+	ssiServer, err := server.NewSSIServer(shutdown, logger, *cfg)
 	if err != nil {
-		log.Fatalf("could not start http services: %s", err.Error())
+		logger.Fatalf("could not start http services: %s", err.Error())
 	}
 	api := http.Server{
-		Addr:         cfg.Web.APIHost,
+		Addr:         cfg.Server.APIHost,
 		Handler:      ssiServer,
-		ReadTimeout:  cfg.Web.ReadTimeout,
-		WriteTimeout: cfg.Web.WriteTimeout,
+		ReadTimeout:  cfg.Server.ReadTimeout,
+		WriteTimeout: cfg.Server.WriteTimeout,
 	}
 
 	serverErrors := make(chan error, 1)
 
 	go func() {
-		log.Printf("main: server started and listening on -> %s", api.Addr)
+		logger.Printf("main: server started and listening on -> %s", api.Addr)
 
 		serverErrors <- api.ListenAndServe()
 	}()
@@ -121,9 +76,9 @@ func run(log *log.Logger) error {
 	case err := <-serverErrors:
 		return errors.Wrap(err, "server error")
 	case sig := <-shutdown:
-		log.Printf("main: shutdown signal received -> %v", sig)
+		logger.Printf("main: shutdown signal received -> %v", sig)
 
-		ctx, cancel := context.WithTimeout(context.Background(), cfg.Web.ShutdownTimeout)
+		ctx, cancel := context.WithTimeout(context.Background(), cfg.Server.ShutdownTimeout)
 		defer cancel()
 
 		if err := api.Shutdown(ctx); err != nil {
diff --git a/config/config.go b/config/config.go
new file mode 100644
index 0000000..f63a26c
--- /dev/null
+++ b/config/config.go
@@ -0,0 +1,137 @@
+package config
+
+import (
+	"fmt"
+	"github.com/BurntSushi/toml"
+	"github.com/ardanlabs/conf"
+	"github.com/pkg/errors"
+	"log"
+	"os"
+	"path/filepath"
+	"reflect"
+	"time"
+)
+
+const (
+	DefaultConfigPath = "config.toml"
+	ServiceName       = "ssi-service"
+	ConfigExtension   = ".toml"
+)
+
+type SSIServiceConfig struct {
+	conf.Version
+	Server   ServerConfig   `toml:"server"`
+	Services ServicesConfig `toml:"services"`
+}
+
+// ServerConfig represents configurable properties for the HTTP server
+type ServerConfig struct {
+	APIHost         string        `toml:"api_host" conf:"default:0.0.0.0:3000"`
+	DebugHost       string        `toml:"debug_host" conf:"default:0.0.0.0:4000"`
+	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"`
+}
+
+// ServicesConfig represents configurable properties for the components of the SSI Service
+type ServicesConfig struct {
+	// at present, it is assumed that a single storage provider works for all services
+	// in the future it may make sense to have per-service storage providers (e.g. mysql for one service,
+	// mongo for another)
+	StorageProvider string `toml:"storage"`
+
+	// Embed all service-specific configs here. The order matters: from which should be instantiated first, to last
+
+	DIDConfig    DIDServiceConfig    `toml:"did,omitempty"`
+	SchemaConfig SchemaServiceConfig `toml:"schema,omitempty"`
+}
+
+// BaseServiceConfig represents configurable properties for a specific component of the SSI Service
+// Can be wrapped and extended for any specific service config
+type BaseServiceConfig struct {
+	Name string `toml:"name"`
+}
+
+type DIDServiceConfig struct {
+	*BaseServiceConfig
+	Methods []string `toml:"methods"`
+}
+
+func (d *DIDServiceConfig) IsEmpty() bool {
+	if d == nil {
+		return true
+	}
+	return reflect.DeepEqual(d, &DIDServiceConfig{})
+}
+
+type SchemaServiceConfig struct {
+	*BaseServiceConfig
+}
+
+func (s *SchemaServiceConfig) IsEmpty() bool {
+	if s == nil {
+		return true
+	}
+	return reflect.DeepEqual(s, &SchemaServiceConfig{})
+}
+
+// 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(logger *log.Logger, path string) (*SSIServiceConfig, error) {
+	// no path, load default config
+	defaultConfig := false
+	if path == "" {
+		logger.Printf("no config path provided, loading default config...")
+		defaultConfig = true
+	} else if filepath.Ext(path) != ConfigExtension {
+		return nil, fmt.Errorf("path<%s> did not match the expected TOML format", path)
+	}
+
+	// create the config object
+	var config SSIServiceConfig
+
+	// parse and apply defaults
+	if err := conf.Parse(os.Args[1:], ServiceName, &config); err != nil {
+		switch err {
+		case conf.ErrHelpWanted:
+			usage, err := conf.Usage(ServiceName, &config)
+			if err != nil {
+				return nil, errors.Wrap(err, "parsing config")
+			}
+			fmt.Println(usage)
+
+			return nil, nil
+
+		case conf.ErrVersionWanted:
+			version, err := conf.VersionString(ServiceName, &config)
+			if err != nil {
+				return nil, errors.Wrap(err, "generating config version")
+			}
+
+			fmt.Println(version)
+			return nil, nil
+		}
+
+		return nil, errors.Wrap(err, "parsing config")
+	}
+
+	if defaultConfig {
+		config.Services = ServicesConfig{
+			StorageProvider: "bolt",
+			DIDConfig: DIDServiceConfig{
+				BaseServiceConfig: &BaseServiceConfig{Name: "did"},
+				Methods:           []string{"key"},
+			},
+			SchemaConfig: SchemaServiceConfig{
+				BaseServiceConfig: &BaseServiceConfig{Name: "schema"},
+			},
+		}
+	} else {
+		// load from TOML file
+		if _, err := toml.DecodeFile(path, &config); err != nil {
+			return nil, errors.Wrapf(err, "could not load config: %s", path)
+		}
+	}
+
+	return &config, nil
+}
diff --git a/config/config.toml b/config/config.toml
new file mode 100644
index 0000000..daf349b
--- /dev/null
+++ b/config/config.toml
@@ -0,0 +1,25 @@
+title = "SSI Service Config"
+
+svn = "0.0.1"
+desc = "https://github.com/TBD54566975/ssi-service"
+
+# http service configuration
+[server]
+api_host = "0.0.0.0:3000"
+debug_host = "0.0.0.0:4000"
+
+# 5 seconds, time is in nanoseconds
+read_timeout = 5000000000
+write_timeout = 5000000000
+shutdown_timeout = 5000000000
+
+[services]
+storage = "bolt"
+
+# per-service configuration
+[services.did]
+name = "did"
+methods = ["key"]
+
+[services.schema]
+name = "schema"
\ No newline at end of file
diff --git a/go.mod b/go.mod
index ecf7e34..cd0f7b1 100644
--- a/go.mod
+++ b/go.mod
@@ -3,6 +3,7 @@ module github.com/tbd54566975/ssi-service
 go 1.17
 
 require (
+	github.com/BurntSushi/toml v1.1.0
 	github.com/TBD54566975/ssi-sdk v0.0.0-20220419032604-6292424ef7ae
 	github.com/ardanlabs/conf v1.5.0
 	github.com/boltdb/bolt v1.3.1
@@ -14,7 +15,7 @@ require (
 	github.com/mr-tron/base58 v1.2.0
 	github.com/pkg/errors v0.9.1
 	github.com/stretchr/testify v1.7.1
-	go.opentelemetry.io/otel/trace v1.6.3
+	go.opentelemetry.io/otel/trace v1.7.0
 	golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4
 	gopkg.in/go-playground/validator.v9 v9.31.0
 )
@@ -50,7 +51,7 @@ require (
 	github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
 	github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
 	github.com/xeipuuv/gojsonschema v1.2.0 // indirect
-	go.opentelemetry.io/otel v1.6.3 // indirect
+	go.opentelemetry.io/otel v1.7.0 // indirect
 	golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect
 	golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 // indirect
 	golang.org/x/text v0.3.7 // indirect
diff --git a/go.sum b/go.sum
index 82c0422..6066338 100644
--- a/go.sum
+++ b/go.sum
@@ -38,6 +38,8 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX
 cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I=
+github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
 github.com/TBD54566975/ssi-sdk v0.0.0-20220419032604-6292424ef7ae h1:8f4RIwlopUS2hmj1ohvRjVnftZGVQzubNenZb/9q+sY=
 github.com/TBD54566975/ssi-sdk v0.0.0-20220419032604-6292424ef7ae/go.mod h1:dZf0M3fNzzqFH31ePuA8XfWQTS0RKEn6cDSjcwS+Ls0=
@@ -362,10 +364,10 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
 go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
-go.opentelemetry.io/otel v1.6.3 h1:FLOfo8f9JzFVFVyU+MSRJc2HdEAXQgm7pIv2uFKRSZE=
-go.opentelemetry.io/otel v1.6.3/go.mod h1:7BgNga5fNlF/iZjG06hM3yofffp0ofKCDwSXx1GC4dI=
-go.opentelemetry.io/otel/trace v1.6.3 h1:IqN4L+5b0mPNjdXIiZ90Ni4Bl5BRkDQywePLWemd9bc=
-go.opentelemetry.io/otel/trace v1.6.3/go.mod h1:GNJQusJlUgZl9/TQBPKU/Y/ty+0iVB5fjhKeJGZPGFs=
+go.opentelemetry.io/otel v1.7.0 h1:Z2lA3Tdch0iDcrhJXDIlC94XE+bxok1F9B+4Lz/lGsM=
+go.opentelemetry.io/otel v1.7.0/go.mod h1:5BdUoMIz5WEs0vt0CUEMtSSaTSHBBVwrhnz7+nrD5xk=
+go.opentelemetry.io/otel/trace v1.7.0 h1:O37Iogk1lEkMRXewVtZ1BBTVn5JEp8GrJvP92bJqC6o=
+go.opentelemetry.io/otel/trace v1.7.0/go.mod h1:fzLSB9nqR2eXzxPXb2JW9IKE+ScyXA48yyE4TNvoHqU=
 go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
 go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
diff --git a/pkg/server/server.go b/pkg/server/server.go
index 874cf29..641fcdd 100644
--- a/pkg/server/server.go
+++ b/pkg/server/server.go
@@ -5,6 +5,7 @@ package server
 import (
 	"fmt"
 	"github.com/pkg/errors"
+	"github.com/tbd54566975/ssi-service/config"
 	"github.com/tbd54566975/ssi-service/pkg/server/framework"
 	svcframework "github.com/tbd54566975/ssi-service/pkg/service/framework"
 
@@ -25,18 +26,18 @@ const (
 
 // SSIServer exposes all dependencies needed to run a http server and all its services
 type SSIServer struct {
+	*log.Logger
 	*framework.Server
+	*config.ServerConfig
 	*service.SSIService
-	*service.Config
 }
 
 // NewSSIServer does two things: instantiates all service and registers their HTTP bindings
-func NewSSIServer(shutdown chan os.Signal, config service.Config) (*SSIServer, error) {
+func NewSSIServer(shutdown chan os.Signal, logger *log.Logger, config config.SSIServiceConfig) (*SSIServer, error) {
 	// creates an HTTP server from the framework, and wrap it to extend it for the SSIS
-	logger := config.Logger
 	middlewares := []framework.Middleware{middleware.Logger(logger), middleware.Errors(logger), middleware.Metrics(), middleware.Panics(logger)}
 	httpServer := framework.NewHTTPServer(shutdown, middlewares...)
-	ssi, err := service.InstantiateSSIService(config)
+	ssi, err := service.InstantiateSSIService(logger, config.Services)
 	if err != nil {
 		return nil, err
 	}
@@ -50,9 +51,10 @@ func NewSSIServer(shutdown chan os.Signal, config service.Config) (*SSIServer, e
 
 	// create the server instance to be returned
 	server := SSIServer{
-		Server:     httpServer,
-		SSIService: ssi,
-		Config:     &config,
+		Logger:       logger,
+		Server:       httpServer,
+		SSIService:   ssi,
+		ServerConfig: &config.Server,
 	}
 
 	// start all services and their routers
diff --git a/pkg/service/did/did.go b/pkg/service/did/did.go
index f0edfcd..95968d9 100644
--- a/pkg/service/did/did.go
+++ b/pkg/service/did/did.go
@@ -3,6 +3,7 @@ package did
 import (
 	"fmt"
 	"github.com/pkg/errors"
+	"github.com/tbd54566975/ssi-service/config"
 	didstorage "github.com/tbd54566975/ssi-service/pkg/service/did/storage"
 	"github.com/tbd54566975/ssi-service/pkg/service/framework"
 	"github.com/tbd54566975/ssi-service/pkg/storage"
@@ -20,6 +21,7 @@ type Service struct {
 	handlers map[Method]MethodHandler
 	storage  didstorage.Storage
 	logger   *log.Logger
+	config   config.DIDServiceConfig
 }
 
 func (s Service) Type() framework.Type {
@@ -37,6 +39,10 @@ func (s Service) Status() framework.Status {
 	return framework.Status{Status: framework.StatusReady}
 }
 
+func (s Service) Config() config.DIDServiceConfig {
+	return s.config
+}
+
 func (s Service) GetSupportedMethods() GetSupportedMethodsResponse {
 	var methods []Method
 	for method := range s.handlers {
@@ -79,7 +85,7 @@ type MethodHandler interface {
 	GetDID(request GetDIDRequest) (*GetDIDResponse, error)
 }
 
-func NewDIDService(log *log.Logger, methods []Method, s storage.ServiceStorage) (*Service, error) {
+func NewDIDService(log *log.Logger, config config.DIDServiceConfig, s storage.ServiceStorage) (*Service, error) {
 	didStorage, err := didstorage.NewDIDStorage(s)
 	if err != nil {
 		return nil, errors.Wrap(err, "could not instantiate DID storage for the DID service")
@@ -91,8 +97,8 @@ func NewDIDService(log *log.Logger, methods []Method, s storage.ServiceStorage)
 	}
 
 	// instantiate all handlers for DID methods
-	for _, m := range methods {
-		if err := svc.instantiateHandlerForMethod(m); err != nil {
+	for _, m := range config.Methods {
+		if err := svc.instantiateHandlerForMethod(Method(m)); err != nil {
 			return nil, errors.Wrap(err, "could not instantiate DID svc")
 		}
 	}
diff --git a/pkg/service/did/model.go b/pkg/service/did/model.go
index 7633976..8625d27 100644
--- a/pkg/service/did/model.go
+++ b/pkg/service/did/model.go
@@ -2,7 +2,7 @@ package did
 
 import (
 	"github.com/TBD54566975/ssi-sdk/crypto"
-	sdkdid "github.com/TBD54566975/ssi-sdk/did"
+	didsdk "github.com/TBD54566975/ssi-sdk/did"
 )
 
 type GetSupportedMethodsResponse struct {
@@ -17,7 +17,7 @@ type CreateDIDRequest struct {
 
 // CreateDIDResponse is the JSON-serializable response for creating a DID
 type CreateDIDResponse struct {
-	DID sdkdid.DIDDocument `json:"did"`
+	DID didsdk.DIDDocument `json:"did"`
 	// TODO(gabe) this is temporary, and should never be exposed like this!
 	PrivateKey string `json:"base58PrivateKey"`
 }
@@ -29,5 +29,5 @@ type GetDIDRequest struct {
 
 // GetDIDResponse is the JSON-serializable response for getting a DID
 type GetDIDResponse struct {
-	DID sdkdid.DIDDocument `json:"did"`
+	DID didsdk.DIDDocument `json:"did"`
 }
diff --git a/pkg/service/framework/framework.go b/pkg/service/framework/framework.go
index 5e1b68a..04c2bf4 100644
--- a/pkg/service/framework/framework.go
+++ b/pkg/service/framework/framework.go
@@ -8,13 +8,17 @@ type (
 const (
 	// List of all service
 
-	DID    Type = "did-service"
-	Schema Type = "schema-service"
+	DID    Type = "did"
+	Schema Type = "schema"
 
 	StatusReady    StatusState = "ready"
 	StatusNotReady StatusState = "not_ready"
 )
 
+func (t Type) String() string {
+	return string(t)
+}
+
 // Status is for service reporting on their status
 type Status struct {
 	Status  StatusState `json:"status,omitempty"`
diff --git a/pkg/service/schema/schema.go b/pkg/service/schema/schema.go
index c35a81d..8bc79a1 100644
--- a/pkg/service/schema/schema.go
+++ b/pkg/service/schema/schema.go
@@ -7,6 +7,7 @@ import (
 	schemalib "github.com/TBD54566975/ssi-sdk/schema"
 	"github.com/google/uuid"
 	"github.com/pkg/errors"
+	"github.com/tbd54566975/ssi-service/config"
 	"github.com/tbd54566975/ssi-service/pkg/service/framework"
 	schemastorage "github.com/tbd54566975/ssi-service/pkg/service/schema/storage"
 	"github.com/tbd54566975/ssi-service/pkg/storage"
@@ -17,6 +18,7 @@ import (
 type Service struct {
 	storage schemastorage.Storage
 	log     *log.Logger
+	config  config.SchemaServiceConfig
 }
 
 func (s Service) Type() framework.Type {
@@ -33,7 +35,11 @@ func (s Service) Status() framework.Status {
 	return framework.Status{Status: framework.StatusReady}
 }
 
-func NewSchemaService(logger *log.Logger, s storage.ServiceStorage) (*Service, error) {
+func (s Service) Config() config.SchemaServiceConfig {
+	return s.config
+}
+
+func NewSchemaService(logger *log.Logger, config config.SchemaServiceConfig, s storage.ServiceStorage) (*Service, error) {
 	schemaStorage, err := schemastorage.NewSchemaStorage(s)
 	if err != nil {
 		return nil, errors.Wrap(err, "could not instantiate Schema storage for the Schema service")
@@ -41,6 +47,7 @@ func NewSchemaService(logger *log.Logger, s storage.ServiceStorage) (*Service, e
 	return &Service{
 		storage: schemaStorage,
 		log:     logger,
+		config:  config,
 	}, nil
 }
 
@@ -57,7 +64,7 @@ func (s Service) CreateSchema(request CreateSchemaRequest) (*CreateSchemaRespons
 	}
 
 	schemaID := uuid.NewString()
-	schema := schema.VCJSONSchema{
+	schemaValue := schema.VCJSONSchema{
 		Type:     schema.VCJSONSchemaType,
 		Version:  Version1,
 		ID:       schemaID,
@@ -67,12 +74,12 @@ func (s Service) CreateSchema(request CreateSchemaRequest) (*CreateSchemaRespons
 		Schema:   request.Schema,
 	}
 
-	storedSchema := schemastorage.StoredSchema{Schema: schema}
+	storedSchema := schemastorage.StoredSchema{Schema: schemaValue}
 	if err := s.storage.StoreSchema(storedSchema); err != nil {
 		return nil, errors.Wrap(err, "could not store schema")
 	}
 
-	return &CreateSchemaResponse{ID: schemaID, Schema: schema}, nil
+	return &CreateSchemaResponse{ID: schemaID, Schema: schemaValue}, nil
 }
 
 func (s Service) GetSchemas() (*GetSchemasResponse, error) {
diff --git a/pkg/service/service.go b/pkg/service/service.go
index 44e8efc..220709a 100644
--- a/pkg/service/service.go
+++ b/pkg/service/service.go
@@ -1,7 +1,9 @@
 package service
 
 import (
+	"fmt"
 	"github.com/pkg/errors"
+	"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/service/schema"
@@ -9,49 +11,64 @@ import (
 	"log"
 )
 
-// Config TODO(gabe) per-service config
-type Config struct {
-	Logger      *log.Logger
-	Environment string `json:"environment,omitempty"`
-	Description string `json:"description,omitempty"`
-}
-
 // SSIService represents all services and their dependencies independent of transport
 type SSIService struct {
+	*log.Logger
 	services []framework.Service
+	config   config.ServicesConfig
 }
 
 // InstantiateSSIService creates a new instance of the SSIS which instantiates all services and their
 // dependencies independent of transport.
-// TODO(gabe) make service loading config-based
-func InstantiateSSIService(config Config) (*SSIService, error) {
-	if config.Logger == nil {
+func InstantiateSSIService(logger *log.Logger, config config.ServicesConfig) (*SSIService, error) {
+	if logger == nil {
 		return nil, errors.New("logger not initialized")
 	}
-	services, err := instantiateServices(config.Logger)
+	if err := validateServiceConfig(config); err != nil {
+		errMsg := fmt.Sprintf("could not instantiate SSI Service, invalid config: %s", err.Error())
+		log.Printf(errMsg)
+		return nil, errors.New(errMsg)
+	}
+	services, err := instantiateServices(logger, config)
 	if err != nil {
-		errMsg := "could not instantiate the verifiable credentials service"
+		errMsg := fmt.Sprintf("could not instantiate the verifiable credentials service: %s", err.Error())
 		log.Printf(errMsg)
-		return nil, errors.Wrap(err, errMsg)
+		return nil, errors.New(errMsg)
 	}
 	return &SSIService{services: services}, nil
 }
 
+func validateServiceConfig(config config.ServicesConfig) error {
+	if !storage.IsStorageAvailable(config.StorageProvider) {
+		return fmt.Errorf("%s storage provider configured, but not available", config.StorageProvider)
+	}
+	if config.DIDConfig.IsEmpty() {
+		return fmt.Errorf("%s no config provided", framework.DID)
+	}
+	if config.SchemaConfig.IsEmpty() {
+		return fmt.Errorf("%s no config provided", framework.Schema)
+	}
+	return nil
+}
+
+// GetServices returns the instantiated service providers
 func (ssi *SSIService) GetServices() []framework.Service {
 	return ssi.services
 }
 
 // instantiateServices begins all instantiates and their dependencies
-func instantiateServices(logger *log.Logger) ([]framework.Service, error) {
-	bolt, err := storage.NewBoltDB(logger)
+func instantiateServices(logger *log.Logger, config config.ServicesConfig) ([]framework.Service, error) {
+	storageProvider, err := storage.NewStorage(storage.Storage(config.StorageProvider), logger)
 	if err != nil {
-		return nil, errors.Wrap(err, "could not instantiate BoltDB")
+		return nil, errors.Wrapf(err, "could not instantiate storage provider: %s", config.StorageProvider)
 	}
-	didService, err := did.NewDIDService(logger, []did.Method{did.KeyMethod}, bolt)
+
+	didService, err := did.NewDIDService(logger, config.DIDConfig, storageProvider)
 	if err != nil {
 		return nil, errors.Wrap(err, "could not instantiate the DID service")
 	}
-	schemaService, err := schema.NewSchemaService(logger, bolt)
+
+	schemaService, err := schema.NewSchemaService(logger, config.SchemaConfig, storageProvider)
 	if err != nil {
 		return nil, errors.Wrap(err, "could not instantiate the schema service")
 	}
diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go
index fa82cc4..66d3efd 100644
--- a/pkg/storage/storage.go
+++ b/pkg/storage/storage.go
@@ -1,5 +1,16 @@
 package storage
 
+import (
+	"fmt"
+	"log"
+)
+
+type Storage string
+
+const (
+	Bolt Storage = "bolt"
+)
+
 // ServiceStorage describes the api for storage independent of DB providers
 type ServiceStorage interface {
 	Close() error
@@ -9,3 +20,29 @@ type ServiceStorage interface {
 	Delete(namespace, key string) error
 	DeleteNamespace(namespace string) error
 }
+
+// NewStorage creates a new storage provider based on the input
+func NewStorage(storageProvider Storage, logger *log.Logger) (ServiceStorage, error) {
+	switch storageProvider {
+	case Bolt:
+		return NewBoltDB(logger)
+	default:
+		return nil, fmt.Errorf("unsupported storage provider: %s", storageProvider)
+	}
+}
+
+// AvailableStorage returns the supported storage providers
+func AvailableStorage() []Storage {
+	return []Storage{Bolt}
+}
+
+// IsStorageAvailable determines whether a given storage provider is available for instantiation
+func IsStorageAvailable(storage string) bool {
+	all := AvailableStorage()
+	for _, s := range all {
+		if storage == string(s) {
+			return true
+		}
+	}
+	return false
+}


Test patch:
diff --git a/config/config_test.go b/config/config_test.go
new file mode 100644
index 0000000..ac03804
--- /dev/null
+++ b/config/config_test.go
@@ -0,0 +1,23 @@
+package config
+
+import (
+	"github.com/stretchr/testify/assert"
+	"log"
+	"os"
+	"testing"
+)
+
+func TestConfig(t *testing.T) {
+	logger := log.New(os.Stdout, "ssi-test", log.LstdFlags)
+	config, err := LoadConfig(logger, DefaultConfigPath)
+	assert.NoError(t, err)
+	assert.NotEmpty(t, config)
+
+	assert.False(t, config.Server.ReadTimeout.String() == "")
+	assert.False(t, config.Server.WriteTimeout.String() == "")
+	assert.False(t, config.Server.ShutdownTimeout.String() == "")
+	assert.False(t, config.Server.APIHost == "")
+	assert.False(t, config.Server.DebugHost == "")
+
+	assert.NotEmpty(t, config.Services.StorageProvider)
+}
diff --git a/pkg/server/router/did_test.go b/pkg/server/router/did_test.go
index d21e471..c76d057 100644
--- a/pkg/server/router/did_test.go
+++ b/pkg/server/router/did_test.go
@@ -3,6 +3,7 @@ package router
 import (
 	"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"
@@ -39,7 +40,8 @@ func TestDIDRouter(t *testing.T) {
 		assert.NoError(tt, err)
 		assert.NotEmpty(tt, bolt)
 
-		didService, err := did.NewDIDService(logger, []did.Method{did.KeyMethod}, bolt)
+		serviceConfig := config.DIDServiceConfig{Methods: []string{string(did.KeyMethod)}}
+		didService, err := did.NewDIDService(logger, serviceConfig, bolt)
 		assert.NoError(tt, err)
 		assert.NotEmpty(tt, didService)
 
@@ -89,3 +91,7 @@ func (s *testService) Type() framework.Type {
 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)}}
+}
diff --git a/pkg/server/router/schema_test.go b/pkg/server/router/schema_test.go
index d3aabd5..4697856 100644
--- a/pkg/server/router/schema_test.go
+++ b/pkg/server/router/schema_test.go
@@ -2,6 +2,7 @@ package router
 
 import (
 	"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/schema"
 	"github.com/tbd54566975/ssi-service/pkg/storage"
@@ -38,7 +39,8 @@ func TestSchemaRouter(t *testing.T) {
 		assert.NoError(tt, err)
 		assert.NotEmpty(tt, bolt)
 
-		schemaService, err := schema.NewSchemaService(logger, bolt)
+		serviceConfig := config.SchemaServiceConfig{BaseServiceConfig: &config.BaseServiceConfig{Name: "schema"}}
+		schemaService, err := schema.NewSchemaService(logger, serviceConfig, bolt)
 		assert.NoError(tt, err)
 		assert.NotEmpty(tt, schemaService)
 
diff --git a/pkg/server/server_test.go b/pkg/server/server_test.go
index 7c97f5b..e6d4aec 100644
--- a/pkg/server/server_test.go
+++ b/pkg/server/server_test.go
@@ -10,9 +10,9 @@ import (
 	"github.com/google/uuid"
 	"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"
 	"github.com/tbd54566975/ssi-service/pkg/service/did"
 	svcframework "github.com/tbd54566975/ssi-service/pkg/service/framework"
 	"github.com/tbd54566975/ssi-service/pkg/service/schema"
@@ -34,7 +34,9 @@ func TestHealthCheckAPI(t *testing.T) {
 
 	shutdown := make(chan os.Signal, 1)
 	logger := log.New(os.Stdout, "ssi-test", log.LstdFlags)
-	server, err := NewSSIServer(shutdown, service.Config{Logger: logger})
+	serviceConfig, err := config.LoadConfig(logger, "")
+	assert.NoError(t, err)
+	server, err := NewSSIServer(shutdown, logger, *serviceConfig)
 	assert.NoError(t, err)
 	assert.NotEmpty(t, server)
 
@@ -61,7 +63,10 @@ func TestReadinessAPI(t *testing.T) {
 
 	shutdown := make(chan os.Signal, 1)
 	logger := log.New(os.Stdout, "ssi-test", log.LstdFlags)
-	server, err := NewSSIServer(shutdown, service.Config{Logger: logger})
+	serviceConfig, err := config.LoadConfig(logger, "")
+	assert.NoError(t, err)
+
+	server, err := NewSSIServer(shutdown, logger, *serviceConfig)
 	assert.NoError(t, err)
 	assert.NotEmpty(t, server)
 
@@ -224,7 +229,8 @@ func newDIDService(t *testing.T) *router.DIDRouter {
 	require.NoError(t, err)
 	require.NotEmpty(t, bolt)
 
-	didService, err := did.NewDIDService(logger, []did.Method{did.KeyMethod}, bolt)
+	serviceConfig := config.DIDServiceConfig{Methods: []string{string(did.KeyMethod)}}
+	didService, err := did.NewDIDService(logger, serviceConfig, bolt)
 	require.NoError(t, err)
 	require.NotEmpty(t, didService)
 
@@ -381,7 +387,7 @@ func newSchemaService(t *testing.T) *router.SchemaRouter {
 	require.NoError(t, err)
 	require.NotEmpty(t, bolt)
 
-	schemaService, err := schema.NewSchemaService(logger, bolt)
+	schemaService, err := schema.NewSchemaService(logger, config.SchemaServiceConfig{}, bolt)
 	require.NoError(t, err)
 	require.NotEmpty(t, schemaService)