From 7fdd5bf61a9c28a70aa61ea9b4500109fb34ff56 Mon Sep 17 00:00:00 2001 From: Andres Uribe Gonzalez Date: Tue, 25 Jul 2023 19:49:51 -0400 Subject: [PATCH 1/8] Added app level encryption feature. --- config/config.go | 42 +++++- config/dev.toml | 4 + config/prod.toml | 8 +- config/test.toml | 6 + doc/STORAGE.md | 43 +++++- pkg/encryption/encryption.go | 164 +++++++++++++++++++++++ pkg/server/router/did_test.go | 16 +++ pkg/server/server_test.go | 6 +- pkg/service/did/web.go | 2 +- pkg/service/keystore/service.go | 40 ++---- pkg/service/keystore/service_test.go | 66 ---------- pkg/service/keystore/storage.go | 167 +++++------------------- pkg/service/manifest/storage/storage.go | 4 +- pkg/service/operation/storage.go | 6 +- pkg/service/presentation/storage.go | 3 +- pkg/service/service.go | 16 ++- pkg/storage/bolt.go | 52 -------- pkg/storage/db_test.go | 25 +++- pkg/storage/encrypt.go | 151 +++++++++++++++++++++ pkg/storage/storage.go | 74 ++++++++++- 20 files changed, 590 insertions(+), 305 deletions(-) create mode 100644 pkg/encryption/encryption.go create mode 100644 pkg/storage/encrypt.go diff --git a/config/config.go b/config/config.go index 7d1433a1a..6ebbc277c 100644 --- a/config/config.go +++ b/config/config.go @@ -72,6 +72,10 @@ type ServicesConfig struct { StorageOptions []storage.Option `toml:"storage_option"` ServiceEndpoint string `toml:"service_endpoint"` + // Application level encryption configuration. Defines how values are encrypted before they are stored in the + // configured KV store. + AppLevelEncryptionConfiguration EncryptionConfig `toml:"storage_encryption,omitempty"` + // Embed all service-specific configs here. The order matters: from which should be instantiated first, to last KeyStoreConfig KeyStoreServiceConfig `toml:"keystore,omitempty"` DIDConfig DIDServiceConfig `toml:"did,omitempty"` @@ -94,14 +98,34 @@ type BaseServiceConfig struct { type KeyStoreServiceConfig struct { *BaseServiceConfig - // The URI for the master key. We use tink for envelope encryption as described in https://github.com/google/tink/blob/9bc2667963e20eb42611b7581e570f0dddf65a2b/docs/KEY-MANAGEMENT.md#key-management-with-tink - // When left empty, then a random key is generated and used. + // Configuration describing the encryption of the private keys that are under ssi-service's custody. + EncryptionConfig +} + +type EncryptionConfig struct { + DisableEncryption bool `toml:"disable_encryption"` + + // The URI for a master key. We use tink for envelope encryption as described in https://github.com/google/tink/blob/9bc2667963e20eb42611b7581e570f0dddf65a2b/docs/KEY-MANAGEMENT.md#key-management-with-tink + // When left empty and DisableEncryption is off, then a random key is generated and used. This random key is persisted unencrypted in the + // configured storage. Production deployments should never leave this field empty. MasterKeyURI string `toml:"master_key_uri"` - // Path for credentials. Required when using an external KMS. More info at https://github.com/google/tink/blob/9bc2667963e20eb42611b7581e570f0dddf65a2b/docs/KEY-MANAGEMENT.md#credentials + // Path for credentials. Required when MasterKeyURI is set. More info at https://github.com/google/tink/blob/9bc2667963e20eb42611b7581e570f0dddf65a2b/docs/KEY-MANAGEMENT.md#credentials KMSCredentialsPath string `toml:"kms_credentials_path"` } +func (e EncryptionConfig) GetMasterKeyURI() string { + return e.MasterKeyURI +} + +func (e EncryptionConfig) GetKMSCredentialsPath() string { + return e.KMSCredentialsPath +} + +func (e EncryptionConfig) EncryptionEnabled() bool { + return !e.DisableEncryption +} + func (k *KeyStoreServiceConfig) IsEmpty() bool { if k == nil { return true @@ -109,6 +133,18 @@ func (k *KeyStoreServiceConfig) IsEmpty() bool { return reflect.DeepEqual(k, &KeyStoreServiceConfig{}) } +func (k *KeyStoreServiceConfig) GetMasterKeyURI() string { + return k.MasterKeyURI +} + +func (k *KeyStoreServiceConfig) GetKMSCredentialsPath() string { + return k.KMSCredentialsPath +} + +func (k *KeyStoreServiceConfig) EncryptionEnabled() bool { + return !k.DisableEncryption +} + type DIDServiceConfig struct { *BaseServiceConfig Methods []string `toml:"methods"` diff --git a/config/dev.toml b/config/dev.toml index a3f41683f..70e87d59b 100644 --- a/config/dev.toml +++ b/config/dev.toml @@ -29,6 +29,10 @@ service_endpoint = "http://localhost:3000" # example bolt config with filepath option storage = "bolt" +[services.storage_encryption] +# encryption +disable_encryption = true + [[services.storage_option]] id = "boltdb-filepath-option" option = "bolt.db" diff --git a/config/prod.toml b/config/prod.toml index 025a003ee..dc11c4413 100644 --- a/config/prod.toml +++ b/config/prod.toml @@ -20,7 +20,12 @@ log_location = "log" log_level = "info" enable_schema_caching = true -enable_allow_all_cors = true +enable_allow_all_cors = false + +[services.storage_encryption] +# master_key_uri = "gcp-kms://projects/*/locations/*/keyRings/*/cryptoKeys/*" +# kms_credentials_path = "credentials.json" +disable_encryption = false # Storage Configuration [services] @@ -38,6 +43,7 @@ option = "password" # per-service configuration [services.keystore] name = "keystore" +disable_encryption = false # master_key_uri = "gcp-kms://projects/*/locations/*/keyRings/*/cryptoKeys/*" # kms_credentials_path = "credentials.json" diff --git a/config/test.toml b/config/test.toml index 141c6a4c6..02399cc05 100644 --- a/config/test.toml +++ b/config/test.toml @@ -25,6 +25,11 @@ log_level = "warn" enable_schema_caching = true enable_allow_all_cors = true +[services.storage_encryption] +# master_key_uri = "gcp-kms://projects/*/locations/*/keyRings/*/cryptoKeys/*" +# kms_credentials_path = "credentials.json" +disable_encryption = false + # Storage Configuration [services] service_endpoint = "http://localhost:8080" @@ -43,6 +48,7 @@ option = "password" name = "keystore" # master_key_uri = "gcp-kms://projects/*/locations/*/keyRings/*/cryptoKeys/*" # kms_credentials_path = "credentials.json" +disable_encryption = false [services.did] name = "did" diff --git a/doc/STORAGE.md b/doc/STORAGE.md index 7d539f51e..ecffb3ecd 100644 --- a/doc/STORAGE.md +++ b/doc/STORAGE.md @@ -70,4 +70,45 @@ For a working example, see this [dev.toml file](https://github.com/TBD54566975/s You need to implement the [ServiceStorage interface](../pkg/storage/storage.go), similar to how [Redis](../pkg/storage/redis.go) is implemented. For an example, see [this PR](https://github.com/TBD54566975/ssi-service/pull/590/files#diff-606358579107e7ad1221525001aed8c776a141d4cc5aab9ef7a3ddbcec10d9f9) -which introduces the SQL based implementation. \ No newline at end of file +which introduces the SQL based implementation. + +## Encryption + +SSI Service supports application level encryption of values before sending them to the configured KV store. Please note +that keys (i.e. the key of the KV store) are not currently encrypted. A MasterKey is used (a.k.a. a Data Encryption Key or DEK). +The MasterKey can be stored in the configured storage system or in an external Key Management System (KMS) like GCP KMS or AWS KMS. +When storing locally, the key will be automatically generated if it doesn't exist already. + +**For production deployments, it is strongly recommended to store the MasterKey in an external KMS.** + +To use an external KMS: +1. Create a symmetric encryption key in your KMS. You MUST select the algorithm that uses AES-256 block cipher in Galois/Counter Mode (GCM). At the time of writing, this is the only algorithm supported by AWS and GCP. +2. Set the `master_key_uri` field of the `[services.storage_encryption]` section using the format described in [tink](https://github.com/google/tink/blob/9bc2667963e20eb42611b7581e570f0dddf65a2b/docs/KEY-MANAGEMENT.md#key-management-systems) + (we use the tink library under the hood). +3. Set the `kms_credentials_path` field of the `[services.storage_encryption]` section to point to your credentials file, according to [this section](https://github.com/google/tink/blob/9bc2667963e20eb42611b7581e570f0dddf65a2b/docs/KEY-MANAGEMENT.md#credentials). +4. Win! + +Below, there is an example snippet of what the TOML configuration should look like. +```toml +[services.storage_encryption] +# Make sure the following values are valid. +master_key_uri = "gcp-kms://projects/*/locations/*/keyRings/*/cryptoKeys/*" +kms_credentials_path = "credentials.json" +disable_encryption = false +``` + +Storing the MasterKey in the configured storage system is done with the following options in your TOML configuration. + +```toml +[services.storage_encryption] +# ensure that master_key_uri is NOT set. +disable_encryption = false +``` + +Disabling app level encryption is also possible using the following options in your TOML configuration: + +```toml +[services.storage_encryption] +# encryption +disable_encryption = true +``` \ No newline at end of file diff --git a/pkg/encryption/encryption.go b/pkg/encryption/encryption.go new file mode 100644 index 000000000..0f9705387 --- /dev/null +++ b/pkg/encryption/encryption.go @@ -0,0 +1,164 @@ +package encryption + +import ( + "context" + "strings" + + util2 "github.com/TBD54566975/ssi-sdk/util" + "github.com/google/tink/go/aead" + "github.com/google/tink/go/core/registry" + "github.com/google/tink/go/integration/awskms" + "github.com/google/tink/go/integration/gcpkms" + "github.com/google/tink/go/keyset" + "github.com/google/tink/go/tink" + "github.com/pkg/errors" + "github.com/tbd54566975/ssi-service/internal/util" + "google.golang.org/api/option" +) + +// Encrypter the interface for any encrypter implementation. +type Encrypter interface { + Encrypt(ctx context.Context, plaintext, contextData []byte) ([]byte, error) +} + +// Decrypter is the interface for any decrypter. May be AEAD or Hybrid. +type Decrypter interface { + // Decrypt decrypts ciphertext. The second parameter may be treated as associated data for AEAD (as abstracted in + // https://datatracker.ietf.org/doc/html/rfc5116), or as contextInfofor HPKE (https://www.rfc-editor.org/rfc/rfc9180.html) + Decrypt(ctx context.Context, ciphertext, contextInfo []byte) ([]byte, error) +} + +type KeyResolver func(ctx context.Context) ([]byte, error) + +type XChaCha20Poly1305Encrypter struct { + keyResolver KeyResolver +} + +func NewXChaCha20Poly1305EncrypterWithKey(key []byte) *XChaCha20Poly1305Encrypter { + return &XChaCha20Poly1305Encrypter{func(ctx context.Context) ([]byte, error) { + return key, nil + }} +} + +func NewXChaCha20Poly1305EncrypterWithKeyResolver(resolver KeyResolver) *XChaCha20Poly1305Encrypter { + return &XChaCha20Poly1305Encrypter{resolver} +} + +func (k XChaCha20Poly1305Encrypter) Encrypt(ctx context.Context, plaintext, _ []byte) ([]byte, error) { + // encrypt key before storing + key, err := k.keyResolver(ctx) + if err != nil { + return nil, errors.Wrap(err, "resolving key") + } + encryptedKey, err := util.XChaCha20Poly1305Encrypt(key, plaintext) + if err != nil { + return nil, util2.LoggingErrorMsgf(err, "could not encrypt key") + } + return encryptedKey, nil +} + +func (k XChaCha20Poly1305Encrypter) Decrypt(ctx context.Context, ciphertext, _ []byte) ([]byte, error) { + if ciphertext == nil { + return nil, nil + } + + key, err := k.keyResolver(ctx) + if err != nil { + return nil, errors.Wrap(err, "resolving key") + } + // decrypt key before unmarshaling + decryptedKey, err := util.XChaCha20Poly1305Decrypt(key, ciphertext) + if err != nil { + return nil, util2.LoggingErrorMsgf(err, "could not decrypt key") + } + + return decryptedKey, nil +} + +var _ Decrypter = (*XChaCha20Poly1305Encrypter)(nil) +var _ Encrypter = (*XChaCha20Poly1305Encrypter)(nil) + +type noopDecrypter struct{} + +func (n noopDecrypter) Decrypt(_ context.Context, ciphertext, _ []byte) ([]byte, error) { + return ciphertext, nil +} + +type noopEncrypter struct{} + +func (n noopEncrypter) Encrypt(_ context.Context, plaintext, _ []byte) ([]byte, error) { + return plaintext, nil +} + +var _ Decrypter = (*noopDecrypter)(nil) +var _ Encrypter = (*noopEncrypter)(nil) + +var ( + NoopDecrypter = noopDecrypter{} + NoopEncrypter = noopEncrypter{} +) + +type wrappedEncrypter struct { + tink.AEAD +} + +func (w wrappedEncrypter) Encrypt(_ context.Context, plaintext, contextData []byte) ([]byte, error) { + return w.AEAD.Encrypt(plaintext, contextData) +} + +var _ Encrypter = (*wrappedEncrypter)(nil) + +type wrappedDecrypter struct { + tink.AEAD +} + +func (w wrappedDecrypter) Decrypt(_ context.Context, ciphertext, contextInfo []byte) ([]byte, error) { + return w.AEAD.Decrypt(ciphertext, contextInfo) +} + +var _ Decrypter = (*wrappedDecrypter)(nil) + +const ( + gcpKMSScheme = "gcp-kms" + awsKMSScheme = "aws-kms" +) + +type ExternalEncryptionConfig interface { + GetMasterKeyURI() string + GetKMSCredentialsPath() string + EncryptionEnabled() bool +} + +func NewExternalEncrypter(ctx context.Context, cfg ExternalEncryptionConfig) (Encrypter, Decrypter, error) { + if !cfg.EncryptionEnabled() { + return NoopEncrypter, NoopDecrypter, nil + } + var client registry.KMSClient + var err error + switch { + case strings.HasPrefix(cfg.GetMasterKeyURI(), gcpKMSScheme): + client, err = gcpkms.NewClientWithOptions(ctx, cfg.GetMasterKeyURI(), option.WithCredentialsFile(cfg.GetKMSCredentialsPath())) + if err != nil { + return nil, nil, errors.Wrap(err, "creating gcp kms client") + } + case strings.HasPrefix(cfg.GetMasterKeyURI(), awsKMSScheme): + client, err = awskms.NewClientWithCredentials(cfg.GetMasterKeyURI(), cfg.GetKMSCredentialsPath()) + if err != nil { + return nil, nil, errors.Wrap(err, "creating aws kms client") + } + default: + return nil, nil, errors.Errorf("master_key_uri value %q is not supported", cfg.GetMasterKeyURI()) + } + // TODO: move client registration to be per request (i.e. when things are encrypted/decrypted). https://github.com/TBD54566975/ssi-service/issues/598 + registry.RegisterKMSClient(client) + dek := aead.AES256GCMKeyTemplate() + kh, err := keyset.NewHandle(aead.KMSEnvelopeAEADKeyTemplate(cfg.GetMasterKeyURI(), dek)) + if err != nil { + return nil, nil, errors.Wrap(err, "creating keyset handle") + } + a, err := aead.New(kh) + if err != nil { + return nil, nil, errors.Wrap(err, "creating aead from key handl") + } + return wrappedEncrypter{a}, wrappedDecrypter{a}, nil +} diff --git a/pkg/server/router/did_test.go b/pkg/server/router/did_test.go index 8952ad9f5..97d25c89d 100644 --- a/pkg/server/router/did_test.go +++ b/pkg/server/router/did_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/tbd54566975/ssi-service/pkg/service/common" "github.com/tbd54566975/ssi-service/pkg/testutil" + "gopkg.in/h2non/gock.v1" "github.com/tbd54566975/ssi-service/config" "github.com/tbd54566975/ssi-service/pkg/service/did" @@ -189,12 +190,22 @@ func TestDIDRouter(t *testing.T) { assert.ElementsMatch(tt, supported.Methods, []didsdk.Method{didsdk.KeyMethod, didsdk.WebMethod}) + gock.Off() + gock.New("https://example.com"). + Get("/.well-known/did.json"). + Reply(200). + BodyString("") // bad key type createOpts := did.CreateWebDIDOptions{DIDWebID: "did:web:example.com"} _, err = didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{Method: didsdk.WebMethod, KeyType: "bad", Options: createOpts}) assert.Error(tt, err) assert.Contains(tt, err.Error(), "could not generate key for did:web") + gock.Off() + gock.New("https://example.com"). + Get("/.well-known/did.json"). + Reply(200). + BodyString("") // good key type createDIDResponse, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{Method: didsdk.WebMethod, KeyType: crypto.Ed25519, Options: createOpts}) assert.NoError(tt, err) @@ -211,6 +222,11 @@ func TestDIDRouter(t *testing.T) { // make sure it's the same value assert.Equal(tt, createDIDResponse.DID.ID, getDIDResponse.DID.ID) + gock.Off() + gock.New("https://tbd.website"). + Get("/.well-known/did.json"). + Reply(200). + BodyString("") // create a second DID createOpts = did.CreateWebDIDOptions{DIDWebID: "did:web:tbd.website"} createDIDResponse2, err := didService.CreateDIDByMethod(context.Background(), did.CreateDIDRequest{Method: didsdk.WebMethod, KeyType: crypto.Ed25519, Options: createOpts}) diff --git a/pkg/server/server_test.go b/pkg/server/server_test.go index 2b1e89c2c..3b13a2032 100644 --- a/pkg/server/server_test.go +++ b/pkg/server/server_test.go @@ -11,7 +11,6 @@ import ( "github.com/TBD54566975/ssi-sdk/credential/exchange" "github.com/gin-gonic/gin" - "github.com/tbd54566975/ssi-service/internal/util" "github.com/tbd54566975/ssi-service/pkg/service/issuance" "github.com/tbd54566975/ssi-service/pkg/service/manifest/model" @@ -230,8 +229,9 @@ func testKeyStoreService(t *testing.T, db storage.ServiceStorage) (*keystore.Ser } // create a keystore service - require.NoError(t, keystore.EnsureServiceKeyExists(serviceConfig, db)) - factory := keystore.NewKeyStoreServiceFactory(serviceConfig, db) + encrypter, decrypter, err := keystore.NewServiceEncryption(db, serviceConfig.EncryptionConfig, keystore.ServiceKeyEncryptionKey) + require.NoError(t, err) + factory := keystore.NewKeyStoreServiceFactory(serviceConfig, db, encrypter, decrypter) keystoreService, err := factory(db) require.NoError(t, err) require.NotEmpty(t, keystoreService) diff --git a/pkg/service/did/web.go b/pkg/service/did/web.go index 11601c995..158b207e8 100644 --- a/pkg/service/did/web.go +++ b/pkg/service/did/web.go @@ -65,7 +65,7 @@ func (h *webHandler) CreateDID(ctx context.Context, request CreateDIDRequest) (* err := didWeb.Validate(ctx) if err != nil { - return nil, errors.Wrap(err, "could not validate did:web") + return nil, errors.Wrap(err, "could not validate if did:web exists externally") } exists, err := h.storage.DIDExists(ctx, opts.DIDWebID) diff --git a/pkg/service/keystore/service.go b/pkg/service/keystore/service.go index a773de0f2..cb8c4b5c2 100644 --- a/pkg/service/keystore/service.go +++ b/pkg/service/keystore/service.go @@ -10,14 +10,13 @@ import ( "github.com/mr-tron/base58" "github.com/pkg/errors" "github.com/sirupsen/logrus" - "golang.org/x/crypto/chacha20poly1305" - - "github.com/tbd54566975/ssi-service/internal/keyaccess" - "github.com/tbd54566975/ssi-service/config" + "github.com/tbd54566975/ssi-service/internal/keyaccess" "github.com/tbd54566975/ssi-service/internal/util" + "github.com/tbd54566975/ssi-service/pkg/encryption" "github.com/tbd54566975/ssi-service/pkg/service/framework" "github.com/tbd54566975/ssi-service/pkg/storage" + "golang.org/x/crypto/chacha20poly1305" ) type ServiceFactory func(storage.Tx) (*Service, error) @@ -50,20 +49,17 @@ func (s Service) Config() config.KeyStoreServiceConfig { } func NewKeyStoreService(config config.KeyStoreServiceConfig, s storage.ServiceStorage) (*Service, error) { - if err := EnsureServiceKeyExists(config, s); err != nil { - return nil, sdkutil.LoggingErrorMsg(err, "initializing keystore") + encrypter, decrypter, err := NewServiceEncryption(s, config.EncryptionConfig, ServiceKeyEncryptionKey) + if err != nil { + return nil, errors.Wrap(err, "creating new encryption") } - factory := NewKeyStoreServiceFactory(config, s) + + factory := NewKeyStoreServiceFactory(config, s, encrypter, decrypter) return factory(s) } -func NewKeyStoreServiceFactory(config config.KeyStoreServiceConfig, s storage.ServiceStorage) ServiceFactory { +func NewKeyStoreServiceFactory(config config.KeyStoreServiceConfig, s storage.ServiceStorage, encrypter encryption.Encrypter, decrypter encryption.Decrypter) ServiceFactory { return func(tx storage.Tx) (*Service, error) { - encrypter, decrypter, err := newEncryption(s, config) - if err != nil { - return nil, errors.Wrap(err, "creating new encryption") - } - // Next, instantiate the key storage keyStoreStorage, err := NewKeyStoreStorage(s, encrypter, decrypter, tx) if err != nil { @@ -180,24 +176,6 @@ func GenerateServiceKey() (key string, err error) { 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, "encrypting 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, "decrypting key with service key") - } - return decryptedKey, nil -} - // Sign fetches the key in the store, and uses it to sign data. Data should be json or json-serializable. func (s Service) Sign(ctx context.Context, keyID string, data any) (*keyaccess.JWT, error) { gotKey, err := s.GetKey(ctx, GetKeyRequest{ID: keyID}) diff --git a/pkg/service/keystore/service_test.go b/pkg/service/keystore/service_test.go index 16c28ba58..7adf5eafe 100644 --- a/pkg/service/keystore/service_test.go +++ b/pkg/service/keystore/service_test.go @@ -12,7 +12,6 @@ import ( "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/storage" ) @@ -23,71 +22,6 @@ func TestGenerateServiceKey(t *testing.T) { assert.NotEmpty(t, key) } -func TestEncryptDecryptAllKeyTypes(t *testing.T) { - serviceKeyEncoded, err := GenerateServiceKey() - 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.NoError(t, err) - assert.EqualValues(t, privKey, privKeyReconstructed) - }) - } -} - func TestStoreAndGetKey(t *testing.T) { keyStore, err := createKeyStoreService(t) assert.NoError(t, err) diff --git a/pkg/service/keystore/storage.go b/pkg/service/keystore/storage.go index ad25ba1d3..a39798a09 100644 --- a/pkg/service/keystore/storage.go +++ b/pkg/service/keystore/storage.go @@ -2,7 +2,6 @@ package keystore import ( "context" - "strings" "time" "github.com/TBD54566975/ssi-sdk/crypto" @@ -10,19 +9,9 @@ import ( sdkutil "github.com/TBD54566975/ssi-sdk/util" "github.com/benbjohnson/clock" "github.com/goccy/go-json" - "github.com/google/tink/go/aead" - "github.com/google/tink/go/core/registry" - "github.com/google/tink/go/integration/awskms" - "github.com/google/tink/go/integration/gcpkms" - "github.com/google/tink/go/keyset" - "github.com/google/tink/go/tink" "github.com/mr-tron/base58" "github.com/pkg/errors" - "google.golang.org/api/option" - - "github.com/tbd54566975/ssi-service/config" - - "github.com/tbd54566975/ssi-service/internal/util" + "github.com/tbd54566975/ssi-service/pkg/encryption" "github.com/tbd54566975/ssi-service/pkg/storage" ) @@ -57,24 +46,26 @@ const ( namespace = "keystore" serviceInternalSuffix = "service-internal" publicNamespaceSuffix = "public-keys" - skKey = "ssi-service-key" keyNotFoundErrMsg = "key not found" + + ServiceKeyEncryptionKey = "ssi-service-key-encryption-key" + ServiceDataEncryptionKey = "ssi-service-data-key" ) var ( - serviceNamespace = storage.Join(namespace, serviceInternalSuffix) - publicKeyNamespace = storage.Join(namespace, publicNamespaceSuffix) + serviceInternalNamespace = storage.Join(namespace, serviceInternalSuffix) + publicKeyNamespace = storage.Join(namespace, publicNamespaceSuffix) ) type Storage struct { db storage.ServiceStorage tx storage.Tx - encrypter Encrypter - decrypter Decrypter + encrypter encryption.Encrypter + decrypter encryption.Decrypter Clock clock.Clock } -func NewKeyStoreStorage(db storage.ServiceStorage, e Encrypter, d Decrypter, writer storage.Tx) (*Storage, error) { +func NewKeyStoreStorage(db storage.ServiceStorage, e encryption.Encrypter, d encryption.Decrypter, writer storage.Tx) (*Storage, error) { s := &Storage{ db: db, encrypter: e, @@ -89,37 +80,16 @@ func NewKeyStoreStorage(db storage.ServiceStorage, e Encrypter, d Decrypter, wri return s, nil } -type wrappedEncrypter struct { - tink.AEAD -} - -func (w wrappedEncrypter) Encrypt(_ context.Context, plaintext, contextData []byte) ([]byte, error) { - return w.AEAD.Encrypt(plaintext, contextData) -} - -type wrappedDecrypter struct { - tink.AEAD -} - -func (w wrappedDecrypter) Decrypt(_ context.Context, ciphertext, contextInfo []byte) ([]byte, error) { - return w.AEAD.Decrypt(ciphertext, contextInfo) -} - -const ( - gcpKMSScheme = "gcp-kms" - awsKMSScheme = "aws-kms" -) - -// EnsureServiceKeyExists makes sure that the service key that will be used for encryption exists. This function is +// ensureEncryptionKeyExists makes sure that the service key that will be used for encryption exists. This function is // idempotent, so that multiple instances of ssi-service can call it on boot. -func EnsureServiceKeyExists(config config.KeyStoreServiceConfig, provider storage.ServiceStorage) error { - if config.MasterKeyURI != "" { +func ensureEncryptionKeyExists(config encryption.ExternalEncryptionConfig, provider storage.ServiceStorage, namespace, encryptionKeyKey string) error { + if config.GetMasterKeyURI() != "" { return nil } watchKeys := []storage.WatchKey{{ Namespace: namespace, - Key: skKey, + Key: encryptionKeyKey, }} ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) @@ -127,7 +97,7 @@ func EnsureServiceKeyExists(config config.KeyStoreServiceConfig, provider storag _, err := provider.Execute(ctx, func(ctx context.Context, tx storage.Tx) (any, error) { // Create the key only if it doesn't already exist. - gotKey, err := getServiceKey(ctx, provider) + gotKey, err := getServiceKey(ctx, provider, namespace, encryptionKeyKey) if gotKey == nil && err.Error() == keyNotFoundErrMsg { serviceKey, err := GenerateServiceKey() if err != nil { @@ -137,7 +107,7 @@ func EnsureServiceKeyExists(config config.KeyStoreServiceConfig, provider storag key := ServiceKey{ Base58Key: serviceKey, } - if err := storeServiceKey(ctx, tx, key); err != nil { + if err := storeServiceKey(ctx, tx, key, namespace, encryptionKeyKey); err != nil { return nil, err } return nil, nil @@ -150,62 +120,41 @@ func EnsureServiceKeyExists(config config.KeyStoreServiceConfig, provider storag return nil } -// newEncryption creates a pair of Encrypter and Decrypter. The service key must have been created before this function -// is called. EnsureServiceKeyExists can be used to make sure the service key exists. -func newEncryption(db storage.ServiceStorage, cfg config.KeyStoreServiceConfig) (Encrypter, Decrypter, error) { - if len(cfg.MasterKeyURI) != 0 { +// NewServiceEncryption creates a pair of Encrypter and Decrypter with the given configuration. +func NewServiceEncryption(db storage.ServiceStorage, cfg encryption.ExternalEncryptionConfig, key string) (encryption.Encrypter, encryption.Decrypter, error) { + if !cfg.EncryptionEnabled() { + return encryption.NoopEncrypter, encryption.NoopDecrypter, nil + } + + if len(cfg.GetMasterKeyURI()) != 0 { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - return NewExternalEncrypter(ctx, cfg) + return encryption.NewExternalEncrypter(ctx, cfg) } - return &encrypter{db}, &decrypter{db}, nil -} - -func NewExternalEncrypter(ctx context.Context, cfg config.KeyStoreServiceConfig) (Encrypter, Decrypter, error) { - var client registry.KMSClient - var err error - switch { - case strings.HasPrefix(cfg.MasterKeyURI, gcpKMSScheme): - client, err = gcpkms.NewClientWithOptions(ctx, cfg.MasterKeyURI, option.WithCredentialsFile(cfg.KMSCredentialsPath)) - if err != nil { - return nil, nil, errors.Wrap(err, "creating gcp kms client") - } - case strings.HasPrefix(cfg.MasterKeyURI, awsKMSScheme): - client, err = awskms.NewClientWithCredentials(cfg.MasterKeyURI, cfg.KMSCredentialsPath) - if err != nil { - return nil, nil, errors.Wrap(err, "creating aws kms client") - } - default: - return nil, nil, errors.Errorf("master_key_uri value %q is not supported", cfg.MasterKeyURI) - } - registry.RegisterKMSClient(client) - dek := aead.AES256GCMKeyTemplate() - kh, err := keyset.NewHandle(aead.KMSEnvelopeAEADKeyTemplate(cfg.MasterKeyURI, dek)) - if err != nil { - return nil, nil, errors.Wrap(err, "creating keyset handle") + if err := ensureEncryptionKeyExists(cfg, db, serviceInternalNamespace, key); err != nil { + return nil, nil, errors.Wrap(err, "ensuring that the encryption key exists") } - a, err := aead.New(kh) - if err != nil { - return nil, nil, errors.Wrap(err, "creating aead from key handl") - } - return wrappedEncrypter{a}, wrappedDecrypter{a}, nil + encSuite := encryption.NewXChaCha20Poly1305EncrypterWithKeyResolver(func(ctx context.Context) ([]byte, error) { + return getServiceKey(ctx, db, serviceInternalNamespace, key) + }) + return encSuite, encSuite, nil } // TODO(gabe): support more robust service key operations, including rotation, and caching -func storeServiceKey(ctx context.Context, tx storage.Tx, key ServiceKey) error { +func storeServiceKey(ctx context.Context, tx storage.Tx, key ServiceKey, namespace string, skKey string) error { keyBytes, err := json.Marshal(key) if err != nil { return sdkutil.LoggingErrorMsg(err, "could not marshal service key") } - if err = tx.Write(ctx, serviceNamespace, skKey, keyBytes); err != nil { + if err = tx.Write(ctx, namespace, skKey, keyBytes); err != nil { return sdkutil.LoggingErrorMsg(err, "could store marshal service key") } return nil } -func getServiceKey(ctx context.Context, db storage.ServiceStorage) ([]byte, error) { - storedKeyBytes, err := db.Read(ctx, serviceNamespace, skKey) +func getServiceKey(ctx context.Context, db storage.ServiceStorage, namespace, skKey string) ([]byte, error) { + storedKeyBytes, err := db.Read(ctx, namespace, skKey) if err != nil { return nil, sdkutil.LoggingErrorMsg(err, "could not get service key") } @@ -226,56 +175,6 @@ func getServiceKey(ctx context.Context, db storage.ServiceStorage) ([]byte, erro return keyBytes, nil } -// Encrypter the interface for any encrypter implementation. -type Encrypter interface { - Encrypt(ctx context.Context, plaintext, contextData []byte) ([]byte, error) -} - -// Decrypter is the interface for any decrypter. May be AEAD or Hybrid. -type Decrypter interface { - // Decrypt decrypts ciphertext. The second parameter may be treated as associated data for AEAD (as abstracted in - // https://datatracker.ietf.org/doc/html/rfc5116), or as contextInfofor HPKE (https://www.rfc-editor.org/rfc/rfc9180.html) - Decrypt(ctx context.Context, ciphertext, contextInfo []byte) ([]byte, error) -} - -type encrypter struct { - db storage.ServiceStorage -} - -func (e encrypter) Encrypt(ctx context.Context, plaintext, _ []byte) ([]byte, error) { - // get service key - serviceKey, err := getServiceKey(ctx, e.db) - if err != nil { - return nil, err - } - // encrypt key before storing - encryptedKey, err := util.XChaCha20Poly1305Encrypt(serviceKey, plaintext) - if err != nil { - return nil, sdkutil.LoggingErrorMsgf(err, "could not encrypt key") - } - return encryptedKey, nil -} - -type decrypter struct { - db storage.ServiceStorage -} - -func (d decrypter) Decrypt(ctx context.Context, ciphertext, _ []byte) ([]byte, error) { - // get service key - serviceKey, err := getServiceKey(ctx, d.db) - if err != nil { - return nil, err - } - - // decrypt key before unmarshaling - decryptedKey, err := util.XChaCha20Poly1305Decrypt(serviceKey, ciphertext) - if err != nil { - return nil, sdkutil.LoggingErrorMsgf(err, "could not decrypt key") - } - - return decryptedKey, nil -} - func (kss *Storage) StoreKey(ctx context.Context, key StoredKey) error { // TODO(gabe): conflict checking on key id id := key.ID diff --git a/pkg/service/manifest/storage/storage.go b/pkg/service/manifest/storage/storage.go index 534f9e30a..e4a4050d5 100644 --- a/pkg/service/manifest/storage/storage.go +++ b/pkg/service/manifest/storage/storage.go @@ -246,7 +246,7 @@ func (ms *Storage) StoreReviewApplication(ctx context.Context, applicationID str if approved { m["status"] = opsubmission.StatusApproved } - if _, err := ms.db.Update(ctx, credential.ApplicationNamespace, applicationID, m); err != nil { + if _, err := storage.Update(ctx, ms.db, credential.ApplicationNamespace, applicationID, m); err != nil { return nil, nil, errors.Wrap(err, "updating application") } @@ -254,7 +254,7 @@ func (ms *Storage) StoreReviewApplication(ctx context.Context, applicationID str return nil, nil, errors.Wrap(err, "storing credential response") } - responseData, operationData, err := ms.db.UpdateValueAndOperation(ctx, responseNamespace, response.ID, + responseData, operationData, err := storage.UpdateValueAndOperation(ctx, ms.db, responseNamespace, response.ID, storage.NewUpdater(m), namespace.FromID(opID), opID, opsubmission.OperationUpdater{UpdaterWithMap: storage.NewUpdater(map[string]any{"done": true})}) if err != nil { diff --git a/pkg/service/operation/storage.go b/pkg/service/operation/storage.go index a49fd5381..c9319eaa7 100644 --- a/pkg/service/operation/storage.go +++ b/pkg/service/operation/storage.go @@ -30,8 +30,9 @@ func (s Storage) CancelOperation(ctx context.Context, id string) (*opstorage.Sto var err error switch { case strings.HasPrefix(id, submission.ParentResource): - _, opData, err = s.db.UpdateValueAndOperation( + _, opData, err = storage.UpdateValueAndOperation( ctx, + s.db, submission.Namespace, opstorage.StatusObjectID(id), storage.NewUpdater(map[string]any{ "status": submission.StatusCancelled, "reason": cancelledReason, @@ -42,8 +43,9 @@ func (s Storage) CancelOperation(ctx context.Context, id string) (*opstorage.Sto }), }) case strings.HasPrefix(id, credential.ParentResource): - _, opData, err = s.db.UpdateValueAndOperation( + _, opData, err = storage.UpdateValueAndOperation( ctx, + s.db, credential.ApplicationNamespace, opstorage.StatusObjectID(id), storage.NewUpdater(map[string]any{ "status": credential.StatusCancelled, "reason": cancelledReason, diff --git a/pkg/service/presentation/storage.go b/pkg/service/presentation/storage.go index 009d33760..431154d15 100644 --- a/pkg/service/presentation/storage.go +++ b/pkg/service/presentation/storage.go @@ -35,8 +35,9 @@ func (ps *Storage) UpdateSubmission(ctx context.Context, id string, approved boo if approved { m["status"] = opsubmission.StatusApproved } - submissionData, operationData, err := ps.db.UpdateValueAndOperation( + submissionData, operationData, err := storage.UpdateValueAndOperation( ctx, + ps.db, opsubmission.Namespace, id, storage.NewUpdater(m), diff --git a/pkg/service/service.go b/pkg/service/service.go index a93e61e2b..ed30828cd 100644 --- a/pkg/service/service.go +++ b/pkg/service/service.go @@ -4,6 +4,7 @@ import ( "fmt" sdkutil "github.com/TBD54566975/ssi-sdk/util" + "github.com/pkg/errors" "github.com/tbd54566975/ssi-service/config" "github.com/tbd54566975/ssi-service/pkg/service/credential" "github.com/tbd54566975/ssi-service/pkg/service/did" @@ -81,20 +82,27 @@ func validateServiceConfig(config config.ServicesConfig) error { // instantiateServices begins all instantiates and their dependencies func instantiateServices(config config.ServicesConfig) (*SSIService, error) { - storageProvider, err := storage.NewStorage(storage.Type(config.StorageProvider), config.StorageOptions...) + unencryptedStorageProvider, err := storage.NewStorage(storage.Type(config.StorageProvider), config.StorageOptions...) if err != nil { return nil, sdkutil.LoggingErrorMsgf(err, "could not instantiate storage provider: %s", config.StorageProvider) } + storageEncrypter, storageDecrypter, err := keystore.NewServiceEncryption(unencryptedStorageProvider, config.AppLevelEncryptionConfiguration, keystore.ServiceDataEncryptionKey) + if err != nil { + return nil, errors.Wrap(err, "creating app level encrypter") + } + storageProvider := storage.NewEncryptedWrapper(unencryptedStorageProvider, storageEncrypter, storageDecrypter) + webhookService, err := webhook.NewWebhookService(config.WebhookConfig, storageProvider) if err != nil { return nil, sdkutil.LoggingErrorMsg(err, "could not instantiate the webhook service") } - if err := keystore.EnsureServiceKeyExists(config.KeyStoreConfig, storageProvider); err != nil { - return nil, sdkutil.LoggingErrorMsg(err, "could not ensure the service key exists") + keyEncrypter, keyDecrypter, err := keystore.NewServiceEncryption(unencryptedStorageProvider, config.KeyStoreConfig.EncryptionConfig, keystore.ServiceKeyEncryptionKey) + if err != nil { + return nil, errors.Wrap(err, "creating keystore encrypter") } - keyStoreServiceFactory := keystore.NewKeyStoreServiceFactory(config.KeyStoreConfig, storageProvider) + keyStoreServiceFactory := keystore.NewKeyStoreServiceFactory(config.KeyStoreConfig, storageProvider, keyEncrypter, keyDecrypter) if err != nil { return nil, sdkutil.LoggingErrorMsg(err, "could not instantiate the keystore service factory") } diff --git a/pkg/storage/bolt.go b/pkg/storage/bolt.go index b7705a45e..2b964432a 100644 --- a/pkg/storage/bolt.go +++ b/pkg/storage/bolt.go @@ -355,55 +355,3 @@ type ResponseSettingUpdater interface { // SetUpdatedResponse sets the response that the Update method will later use to modify the data. SetUpdatedResponse([]byte) } - -// UpdateValueAndOperation updates the value stored in (namespace,key) with the new values specified in the map. -// The updated value is then stored inside the (opNamespace, opKey), and the "done" value is set to true. -func (b *BoltDB) UpdateValueAndOperation(_ context.Context, namespace, key string, updater Updater, opNamespace, opKey string, opUpdater ResponseSettingUpdater) (first, op []byte, err error) { - err = b.db.Update(func(tx *bolt.Tx) error { - if err = updateTxFn(namespace, key, updater, &first)(tx); err != nil { - return err - } - opUpdater.SetUpdatedResponse(first) - return updateTxFn(opNamespace, opKey, opUpdater, &op)(tx) - }) - return first, op, err -} - -func (b *BoltDB) Update(_ context.Context, namespace string, key string, values map[string]any) ([]byte, error) { - var updatedData []byte - err := b.db.Update(updateTxFn(namespace, key, NewUpdater(values), &updatedData)) - return updatedData, err -} - -func updateTxFn(namespace string, key string, updater Updater, updatedData *[]byte) func(tx *bolt.Tx) error { - return func(tx *bolt.Tx) error { - data, err := updateTx(tx, namespace, key, updater) - if err != nil { - return err - } - *updatedData = data - return nil - } -} - -func updateTx(tx *bolt.Tx, namespace string, key string, updater Updater) ([]byte, error) { - bucket := tx.Bucket([]byte(namespace)) - if bucket == nil { - return nil, sdkutil.LoggingNewErrorf("namespace<%s> does not exist", namespace) - } - v := bucket.Get([]byte(key)) - if v == nil { - return nil, sdkutil.LoggingNewErrorf("key not found %s", key) - } - if err := updater.Validate(v); err != nil { - return nil, sdkutil.LoggingErrorMsg(err, "validating update") - } - data, err := updater.Update(v) - if err != nil { - return nil, err - } - if err = bucket.Put([]byte(key), data); err != nil { - return nil, errors.Wrap(err, "writing to db") - } - return data, nil -} diff --git a/pkg/storage/db_test.go b/pkg/storage/db_test.go index 5c8980b53..17151c9ea 100644 --- a/pkg/storage/db_test.go +++ b/pkg/storage/db_test.go @@ -15,6 +15,7 @@ import ( "github.com/goccy/go-json" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/tbd54566975/ssi-service/pkg/encryption" ) func getDBImplementations(t *testing.T) []ServiceStorage { @@ -29,6 +30,13 @@ func getDBImplementations(t *testing.T) []ServiceStorage { postgresDB := setupPostgresDB(t) dbImpls = append(dbImpls, postgresDB) + key := make([]byte, 32) + dbImpls = append(dbImpls, NewEncryptedWrapper( + boltDB, + encryption.NewXChaCha20Poly1305EncrypterWithKey(key), + encryption.NewXChaCha20Poly1305EncrypterWithKey(key), + )) + return dbImpls } @@ -498,7 +506,7 @@ func TestDB_Update(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - data, err = db.Update(context.Background(), namespace, tt.args.key, tt.args.values) + data, err = Update(context.Background(), db, namespace, tt.args.key, tt.args.values) if !tt.expectedError(t, err) { return } @@ -520,6 +528,19 @@ func (f testOpUpdater) SetUpdatedResponse(bytes []byte) { f.UpdaterWithMap.Values["response"] = bytes } +func TestDB_Execute(t *testing.T) { + for _, dbImpl := range getDBImplementations(t) { + db := dbImpl + _, err := db.Execute(context.Background(), func(ctx context.Context, tx Tx) (any, error) { + return nil, tx.Write(ctx, "hello", "my_key", []byte(`some bytes`)) + }, nil) + assert.NoError(t, err) + result, err := db.Read(context.Background(), "hello", "my_key") + assert.NoError(t, err) + assert.Equal(t, []byte(`some bytes`), result) + } +} + func TestDB_UpdatedSubmissionAndOperationTxFn(t *testing.T) { for _, dbImpl := range getDBImplementations(t) { db := dbImpl @@ -609,7 +630,7 @@ func TestDB_UpdatedSubmissionAndOperationTxFn(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gotFirstData, gotOpData, err := db.UpdateValueAndOperation(context.Background(), tt.args.namespace, tt.args.key, tt.args.updater, tt.args.opNamespace, tt.args.opKey, testOpUpdater{ + gotFirstData, gotOpData, err := UpdateValueAndOperation(context.Background(), db, tt.args.namespace, tt.args.key, tt.args.updater, tt.args.opNamespace, tt.args.opKey, testOpUpdater{ NewUpdater(map[string]any{ "done": true, }), diff --git a/pkg/storage/encrypt.go b/pkg/storage/encrypt.go new file mode 100644 index 000000000..006f6ac13 --- /dev/null +++ b/pkg/storage/encrypt.go @@ -0,0 +1,151 @@ +package storage + +import ( + "context" + + "github.com/pkg/errors" + "github.com/tbd54566975/ssi-service/pkg/encryption" +) + +type EncryptedWrapper struct { + s ServiceStorage + encrypter encryption.Encrypter + decrypter encryption.Decrypter +} + +func NewEncryptedWrapper(s ServiceStorage, encrypter encryption.Encrypter, decrypter encryption.Decrypter) *EncryptedWrapper { + return &EncryptedWrapper{ + s: s, + encrypter: encrypter, + decrypter: decrypter, + } +} + +func (e EncryptedWrapper) Init(opts ...Option) error { + return e.s.Init(opts...) +} + +func (e EncryptedWrapper) Type() Type { + return e.s.Type() +} + +func (e EncryptedWrapper) URI() string { + return e.s.URI() +} + +func (e EncryptedWrapper) IsOpen() bool { + return e.s.IsOpen() +} + +func (e EncryptedWrapper) Close() error { + return e.s.Close() +} + +func (e EncryptedWrapper) Write(ctx context.Context, namespace, key string, value []byte) error { + encryptedData, err := e.encrypter.Encrypt(ctx, value, nil) + if err != nil { + return errors.Wrap(err, "encrypting data") + } + return e.s.Write(ctx, namespace, key, encryptedData) +} + +func (e EncryptedWrapper) WriteMany(ctx context.Context, namespace, keys []string, values [][]byte) error { + encryptedValues := make([][]byte, 0, len(values)) + for _, value := range values { + encryptedData, err := e.encrypter.Encrypt(ctx, value, nil) + if err != nil { + return errors.Wrap(err, "encrypting data") + } + encryptedValues = append(encryptedValues, encryptedData) + } + return e.s.WriteMany(ctx, namespace, keys, encryptedValues) +} + +func (e EncryptedWrapper) Read(ctx context.Context, namespace, key string) ([]byte, error) { + storedBytes, err := e.s.Read(ctx, namespace, key) + if err != nil { + return nil, err + } + decryptedData, err := e.decrypter.Decrypt(ctx, storedBytes, nil) + if err != nil { + return nil, errors.Wrap(err, "decrypting data") + } + return decryptedData, nil +} + +func (e EncryptedWrapper) Exists(ctx context.Context, namespace, key string) (bool, error) { + return e.s.Exists(ctx, namespace, key) +} + +func (e EncryptedWrapper) ReadAll(ctx context.Context, namespace string) (map[string][]byte, error) { + encryptedKeyedBytes, err := e.s.ReadAll(ctx, namespace) + if err != nil { + return nil, err + } + return e.decryptMap(ctx, encryptedKeyedBytes) +} + +func (e EncryptedWrapper) decryptMap(ctx context.Context, encryptedKeyedBytes map[string][]byte) (map[string][]byte, error) { + decryptedValues := make(map[string][]byte, len(encryptedKeyedBytes)) + for key, encryptedBytes := range encryptedKeyedBytes { + decryptedData, err := e.decrypter.Decrypt(ctx, encryptedBytes, nil) + if err != nil { + return nil, errors.Wrap(err, "decrypting data") + } + decryptedValues[key] = decryptedData + } + return decryptedValues, nil +} + +func (e EncryptedWrapper) ReadPage(ctx context.Context, namespace string, pageToken string, pageSize int) (results map[string][]byte, nextPageToken string, err error) { + encryptedResults, nextPageToken, err := e.s.ReadPage(ctx, namespace, pageToken, pageSize) + if err != nil { + return nil, "", err + } + decryptedMap, err := e.decryptMap(ctx, encryptedResults) + if err != nil { + return nil, "", err + } + return decryptedMap, nextPageToken, err +} + +func (e EncryptedWrapper) ReadPrefix(ctx context.Context, namespace, prefix string) (map[string][]byte, error) { + encryptedMap, err := e.s.ReadPrefix(ctx, namespace, prefix) + if err != nil { + return nil, err + } + return e.decryptMap(ctx, encryptedMap) +} + +func (e EncryptedWrapper) ReadAllKeys(ctx context.Context, namespace string) ([]string, error) { + return e.s.ReadAllKeys(ctx, namespace) +} + +func (e EncryptedWrapper) Delete(ctx context.Context, namespace, key string) error { + return e.s.Delete(ctx, namespace, key) +} + +func (e EncryptedWrapper) DeleteNamespace(ctx context.Context, namespace string) error { + return e.s.DeleteNamespace(ctx, namespace) +} + +type encryptedTx struct { + tx Tx + encrypter encryption.Encrypter +} + +func (m encryptedTx) Write(ctx context.Context, namespace, key string, value []byte) error { + encryptedData, err := m.encrypter.Encrypt(ctx, value, nil) + if err != nil { + return errors.Wrap(err, "encrypting data") + } + return m.tx.Write(ctx, namespace, key, encryptedData) +} + +func (e EncryptedWrapper) Execute(ctx context.Context, businessLogicFunc BusinessLogicFunc, watchKeys []WatchKey) (any, error) { + return e.s.Execute(ctx, func(ctx context.Context, tx Tx) (any, error) { + return businessLogicFunc(ctx, encryptedTx{tx: tx, encrypter: e.encrypter}) + }, watchKeys) +} + +var _ ServiceStorage = (*EncryptedWrapper)(nil) diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go index 6674a60d7..89131e00f 100644 --- a/pkg/storage/storage.go +++ b/pkg/storage/storage.go @@ -6,6 +6,7 @@ import ( "reflect" "strings" + "github.com/pkg/errors" "github.com/sirupsen/logrus" ) @@ -71,8 +72,6 @@ type ServiceStorage interface { ReadAllKeys(ctx context.Context, namespace string) ([]string, error) Delete(ctx context.Context, namespace, key string) error DeleteNamespace(ctx context.Context, namespace string) error - Update(ctx context.Context, namespace string, key string, values map[string]any) ([]byte, error) - UpdateValueAndOperation(ctx context.Context, namespace, key string, updater Updater, opNamespace, opKey string, opUpdater ResponseSettingUpdater) (first, op []byte, err error) Execute(ctx context.Context, businessLogicFunc BusinessLogicFunc, watchKeys []WatchKey) (any, error) } @@ -137,3 +136,74 @@ func Join(parts ...string) string { func MakeNamespace(ns ...string) string { return strings.Join(ns, "-") } + +// UpdateValueAndOperation updates the value stored in (namespace,key) with the new values specified in the map. +// The updated value is then stored inside the (opNamespace, opKey), and the "done" value is set to true. +func UpdateValueAndOperation(ctx context.Context, s ServiceStorage, namespace, key string, updater Updater, opNamespace, opKey string, opUpdater ResponseSettingUpdater) (first, op []byte, err error) { + type pair struct { + first []byte + second []byte + } + watchKeys := []WatchKey{ + { + Namespace: namespace, + Key: key, + }, + { + Namespace: opNamespace, + Key: opKey, + }, + } + exec, err := s.Execute(ctx, func(ctx context.Context, tx Tx) (any, error) { + first, err = update(ctx, s, tx, namespace, key, updater) + if err != nil { + return nil, err + } + opUpdater.SetUpdatedResponse(first) + op, err = update(ctx, s, tx, opNamespace, opKey, opUpdater) + if err != nil { + return nil, err + } + return &pair{first: first, second: op}, err + }, watchKeys) + if err != nil { + return nil, nil, err + } + execPair := exec.(*pair) + return execPair.first, execPair.second, nil +} + +func Update(ctx context.Context, s ServiceStorage, namespace, key string, m map[string]any) ([]byte, error) { + watchKeys := []WatchKey{ + { + Namespace: namespace, + Key: key, + }, + } + exec, err := s.Execute(ctx, func(ctx context.Context, tx Tx) (any, error) { + return update(ctx, s, tx, namespace, key, NewUpdater(m)) + }, watchKeys) + if err != nil { + return nil, err + } + execBytes := exec.([]byte) + return execBytes, nil +} + +func update(ctx context.Context, s ServiceStorage, tx Tx, namespace, key string, updater Updater) ([]byte, error) { + readData, err := s.Read(ctx, namespace, key) + if err != nil { + return nil, err + } + if err = updater.Validate(readData); err != nil { + return nil, errors.Wrap(err, "validating update") + } + updatedData, err := updater.Update(readData) + if err != nil { + return nil, err + } + if err = tx.Write(ctx, namespace, key, updatedData); err != nil { + return nil, errors.Wrap(err, "writing to db") + } + return updatedData, nil +} From a0618707dae410d6cb44569c63fd0f16607f11ff Mon Sep 17 00:00:00 2001 From: Andres Uribe Gonzalez Date: Wed, 26 Jul 2023 03:28:06 -0400 Subject: [PATCH 2/8] Add validation of config parameters. --- cmd/ssiservice/main.go | 2 +- config/config.go | 32 +++++++++++++++++++++++++++---- config/config_test.go | 30 ++++++++++++++++++++--------- config/testdata/test1.toml | 6 ++++++ pkg/server/server_test.go | 4 ++-- pkg/server/server_webhook_test.go | 2 +- 6 files changed, 59 insertions(+), 17 deletions(-) create mode 100644 config/testdata/test1.toml diff --git a/cmd/ssiservice/main.go b/cmd/ssiservice/main.go index 796e13057..9db1fc83a 100644 --- a/cmd/ssiservice/main.go +++ b/cmd/ssiservice/main.go @@ -52,7 +52,7 @@ func run() error { logrus.Infof("loading config from env var path: %s", envConfigPath) configPath = envConfigPath } - cfg, err := config.LoadConfig(configPath) + cfg, err := config.LoadConfig(configPath, os.DirFS(".")) if err != nil { logrus.Fatalf("could not instantiate config: %s", err.Error()) } diff --git a/config/config.go b/config/config.go index 6ebbc277c..370256be4 100644 --- a/config/config.go +++ b/config/config.go @@ -2,6 +2,7 @@ package config import ( "fmt" + "io/fs" "os" "path/filepath" "reflect" @@ -247,7 +248,10 @@ func (p *WebhookServiceConfig) IsEmpty() bool { // 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) { +func LoadConfig(path string, fs fs.FS) (*SSIServiceConfig, error) { + if fs == nil { + fs = os.DirFS(".") + } loadDefaultConfig, err := checkValidConfigPath(path) if err != nil { return nil, errors.Wrap(err, "validate config path") @@ -262,7 +266,7 @@ func LoadConfig(path string) (*SSIServiceConfig, error) { if loadDefaultConfig { defaultServicesConfig := getDefaultServicesConfig() config.Services = defaultServicesConfig - } else if err = loadTOMLConfig(path, &config); err != nil { + } else if err = loadTOMLConfig(path, &config, fs); err != nil { return nil, errors.Wrap(err, "load toml config") } @@ -270,9 +274,25 @@ func LoadConfig(path string) (*SSIServiceConfig, error) { return nil, errors.Wrap(err, "apply env variables") } + if err = validateConfig(&config); err != nil { + return nil, errors.Wrap(err, "validating config values") + } + return &config, nil } +func validateConfig(s *SSIServiceConfig) error { + if s.Server.Environment == EnvironmentProd { + if s.Services.KeyStoreConfig.DisableEncryption { + return errors.New("prod environment cannot disable key encryption") + } + if s.Services.AppLevelEncryptionConfiguration.DisableEncryption { + logrus.Warn("prod environment detected without app level encryption. This is strongly discouraged.") + } + } + return nil +} + func checkValidConfigPath(path string) (bool, error) { // no path, load default config defaultConfig := false @@ -350,9 +370,13 @@ func getDefaultServicesConfig() ServicesConfig { } } -func loadTOMLConfig(path string, config *SSIServiceConfig) error { +func loadTOMLConfig(path string, config *SSIServiceConfig, fs fs.FS) error { // load from TOML file - if _, err := toml.DecodeFile(path, &config); err != nil { + file, err := fs.Open(path) + if err != nil { + return errors.Wrapf(err, "opening path %s", path) + } + if _, err := toml.NewDecoder(file).Decode(&config); err != nil { return errors.Wrapf(err, "could not load config: %s", path) } diff --git a/config/config_test.go b/config/config_test.go index 12cefe6da..d629f9470 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -1,20 +1,32 @@ package config import ( + "embed" "testing" "github.com/stretchr/testify/assert" ) -func TestConfig(t *testing.T) { - config, err := LoadConfig(Filename) - assert.NoError(t, err) - assert.NotEmpty(t, config) +//go:embed testdata +var testdata embed.FS - 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 == "") +func TestLoadConfig(t *testing.T) { + t.Run("returns no errors when passed in file", func(t *testing.T) { + config, err := LoadConfig(Filename, nil) + assert.NoError(t, err) + assert.NotEmpty(t, config) - assert.NotEmpty(t, config.Services.StorageProvider) + 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.NotEmpty(t, config.Services.StorageProvider) + }) + + t.Run("returns errors when prod disables encryption", func(t *testing.T) { + _, err := LoadConfig("testdata/test1.toml", testdata) + assert.Error(t, err) + assert.ErrorContains(t, err, "prod environment cannot disable key encryption") + }) } diff --git a/config/testdata/test1.toml b/config/testdata/test1.toml new file mode 100644 index 000000000..f96f2191e --- /dev/null +++ b/config/testdata/test1.toml @@ -0,0 +1,6 @@ +[server] +env = "prod" # either 'dev', 'test', or 'prod' + +[services.keystore] +name = "keystore" +disable_encryption = true \ No newline at end of file diff --git a/pkg/server/server_test.go b/pkg/server/server_test.go index 3b13a2032..dca596fad 100644 --- a/pkg/server/server_test.go +++ b/pkg/server/server_test.go @@ -48,7 +48,7 @@ func TestMain(t *testing.M) { func TestHealthCheckAPI(t *testing.T) { shutdown := make(chan os.Signal, 1) - serviceConfig, err := config.LoadConfig("") + serviceConfig, err := config.LoadConfig("", nil) assert.NoError(t, err) server, err := NewSSIServer(shutdown, *serviceConfig) assert.NoError(t, err) @@ -76,7 +76,7 @@ func TestReadinessAPI(t *testing.T) { }) shutdown := make(chan os.Signal, 1) - serviceConfig, err := config.LoadConfig("") + serviceConfig, err := config.LoadConfig("", nil) assert.NoError(t, err) serviceConfig.Services.StorageOptions = []storage.Option{ { diff --git a/pkg/server/server_webhook_test.go b/pkg/server/server_webhook_test.go index ec6f15300..09d7229c3 100644 --- a/pkg/server/server_webhook_test.go +++ b/pkg/server/server_webhook_test.go @@ -48,7 +48,7 @@ func TestSimpleWebhook(t *testing.T) { defer testServer.Close() shutdown := make(chan os.Signal, 1) - serviceConfig, err := config.LoadConfig("") + serviceConfig, err := config.LoadConfig("", nil) assert.NoError(t, err) serviceConfig.Server.APIHost = "0.0.0.0:" + freePort() From 20c1023f838e7b7e4e152597be370973e8ab69aa Mon Sep 17 00:00:00 2001 From: Andres Uribe Gonzalez Date: Wed, 26 Jul 2023 03:46:23 -0400 Subject: [PATCH 3/8] test --- pkg/encryption/encryption_test.go | 93 +++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 pkg/encryption/encryption_test.go diff --git a/pkg/encryption/encryption_test.go b/pkg/encryption/encryption_test.go new file mode 100644 index 000000000..ad1eddbe4 --- /dev/null +++ b/pkg/encryption/encryption_test.go @@ -0,0 +1,93 @@ +package encryption + +import ( + "context" + "testing" + + "github.com/TBD54566975/ssi-sdk/crypto" + sdkutil "github.com/TBD54566975/ssi-sdk/util" + "github.com/mr-tron/base58" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/tbd54566975/ssi-service/internal/util" + "golang.org/x/crypto/chacha20poly1305" +) + +func createServiceKey() (key string, err error) { + keyBytes, err := util.GenerateSalt(chacha20poly1305.KeySize) + if err != nil { + err = errors.Wrap(err, "generating bytes for service key") + return "", sdkutil.LoggingError(err) + } + + key = base58.Encode(keyBytes) + return +} + +func TestEncryptDecryptAllKeyTypes(t *testing.T) { + serviceKeyEncoded, err := createServiceKey() + assert.NoError(t, err) + serviceKey, err := base58.Decode(serviceKeyEncoded) + assert.NoError(t, err) + assert.NotEmpty(t, serviceKey) + encrypter := NewXChaCha20Poly1305EncrypterWithKeyResolver(func(ctx context.Context) ([]byte, error) { + return serviceKey, nil + }) + + 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 := encrypter.Encrypt(context.Background(), privKeyBytes, nil) + assert.NoError(t, err) + assert.NotEmpty(t, encryptedKey) + + // decrypt the serviceKey using our service serviceKey + decryptedKey, err := encrypter.Decrypt(context.Background(), encryptedKey, nil) + assert.NoError(t, err) + assert.NotEmpty(t, decryptedKey) + + // reconstruct the key from its serialized form + privKeyReconstructed, err := crypto.BytesToPrivKey(decryptedKey, test.kt) + assert.NoError(t, err) + assert.EqualValues(t, privKey, privKeyReconstructed) + }) + } +} From 5c6a6002366a76d0c7dd9d8eb43fca6e22675e60 Mon Sep 17 00:00:00 2001 From: Andres Uribe Gonzalez Date: Wed, 26 Jul 2023 03:46:30 -0400 Subject: [PATCH 4/8] better name --- pkg/service/keystore/storage.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/service/keystore/storage.go b/pkg/service/keystore/storage.go index a39798a09..30d8cf9cb 100644 --- a/pkg/service/keystore/storage.go +++ b/pkg/service/keystore/storage.go @@ -82,14 +82,14 @@ func NewKeyStoreStorage(db storage.ServiceStorage, e encryption.Encrypter, d enc // ensureEncryptionKeyExists makes sure that the service key that will be used for encryption exists. This function is // idempotent, so that multiple instances of ssi-service can call it on boot. -func ensureEncryptionKeyExists(config encryption.ExternalEncryptionConfig, provider storage.ServiceStorage, namespace, encryptionKeyKey string) error { +func ensureEncryptionKeyExists(config encryption.ExternalEncryptionConfig, provider storage.ServiceStorage, namespace, encryptionMaterialKey string) error { if config.GetMasterKeyURI() != "" { return nil } watchKeys := []storage.WatchKey{{ Namespace: namespace, - Key: encryptionKeyKey, + Key: encryptionMaterialKey, }} ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) @@ -97,7 +97,7 @@ func ensureEncryptionKeyExists(config encryption.ExternalEncryptionConfig, provi _, err := provider.Execute(ctx, func(ctx context.Context, tx storage.Tx) (any, error) { // Create the key only if it doesn't already exist. - gotKey, err := getServiceKey(ctx, provider, namespace, encryptionKeyKey) + gotKey, err := getServiceKey(ctx, provider, namespace, encryptionMaterialKey) if gotKey == nil && err.Error() == keyNotFoundErrMsg { serviceKey, err := GenerateServiceKey() if err != nil { @@ -107,7 +107,7 @@ func ensureEncryptionKeyExists(config encryption.ExternalEncryptionConfig, provi key := ServiceKey{ Base58Key: serviceKey, } - if err := storeServiceKey(ctx, tx, key, namespace, encryptionKeyKey); err != nil { + if err := storeServiceKey(ctx, tx, key, namespace, encryptionMaterialKey); err != nil { return nil, err } return nil, nil From bbe035423982c637873ac21727ae72bfa8d9bf99 Mon Sep 17 00:00:00 2001 From: Andres Uribe Gonzalez Date: Wed, 26 Jul 2023 04:18:21 -0400 Subject: [PATCH 5/8] naming --- pkg/encryption/encryption.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/encryption/encryption.go b/pkg/encryption/encryption.go index 0f9705387..64a0feba1 100644 --- a/pkg/encryption/encryption.go +++ b/pkg/encryption/encryption.go @@ -4,7 +4,7 @@ import ( "context" "strings" - util2 "github.com/TBD54566975/ssi-sdk/util" + sdkutil "github.com/TBD54566975/ssi-sdk/util" "github.com/google/tink/go/aead" "github.com/google/tink/go/core/registry" "github.com/google/tink/go/integration/awskms" @@ -52,7 +52,7 @@ func (k XChaCha20Poly1305Encrypter) Encrypt(ctx context.Context, plaintext, _ [] } encryptedKey, err := util.XChaCha20Poly1305Encrypt(key, plaintext) if err != nil { - return nil, util2.LoggingErrorMsgf(err, "could not encrypt key") + return nil, sdkutil.LoggingErrorMsgf(err, "could not encrypt key") } return encryptedKey, nil } @@ -69,7 +69,7 @@ func (k XChaCha20Poly1305Encrypter) Decrypt(ctx context.Context, ciphertext, _ [ // decrypt key before unmarshaling decryptedKey, err := util.XChaCha20Poly1305Decrypt(key, ciphertext) if err != nil { - return nil, util2.LoggingErrorMsgf(err, "could not decrypt key") + return nil, sdkutil.LoggingErrorMsgf(err, "could not decrypt key") } return decryptedKey, nil From bf0c2bacd7995710838a66f09805c562f3076be3 Mon Sep 17 00:00:00 2001 From: Andres Uribe Gonzalez Date: Wed, 26 Jul 2023 04:18:49 -0400 Subject: [PATCH 6/8] nil encrypter for kek --- pkg/service/keystore/storage.go | 8 +++++++- pkg/service/service.go | 5 ++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/pkg/service/keystore/storage.go b/pkg/service/keystore/storage.go index 30d8cf9cb..097c893ef 100644 --- a/pkg/service/keystore/storage.go +++ b/pkg/service/keystore/storage.go @@ -76,6 +76,12 @@ func NewKeyStoreStorage(db storage.ServiceStorage, e encryption.Encrypter, d enc if writer != nil { s.tx = writer } + if s.encrypter == nil { + s.encrypter = encryption.NoopEncrypter + } + if s.decrypter == nil { + s.decrypter = encryption.NoopDecrypter + } return s, nil } @@ -123,7 +129,7 @@ func ensureEncryptionKeyExists(config encryption.ExternalEncryptionConfig, provi // NewServiceEncryption creates a pair of Encrypter and Decrypter with the given configuration. func NewServiceEncryption(db storage.ServiceStorage, cfg encryption.ExternalEncryptionConfig, key string) (encryption.Encrypter, encryption.Decrypter, error) { if !cfg.EncryptionEnabled() { - return encryption.NoopEncrypter, encryption.NoopDecrypter, nil + return nil, nil, nil } if len(cfg.GetMasterKeyURI()) != 0 { diff --git a/pkg/service/service.go b/pkg/service/service.go index ed30828cd..d3962bc36 100644 --- a/pkg/service/service.go +++ b/pkg/service/service.go @@ -91,7 +91,10 @@ func instantiateServices(config config.ServicesConfig) (*SSIService, error) { if err != nil { return nil, errors.Wrap(err, "creating app level encrypter") } - storageProvider := storage.NewEncryptedWrapper(unencryptedStorageProvider, storageEncrypter, storageDecrypter) + storageProvider := unencryptedStorageProvider + if storageEncrypter != nil && storageDecrypter != nil { + storageProvider = storage.NewEncryptedWrapper(unencryptedStorageProvider, storageEncrypter, storageDecrypter) + } webhookService, err := webhook.NewWebhookService(config.WebhookConfig, storageProvider) if err != nil { From df7c216ec45250a818330c2a4d2e9e6da20947ba Mon Sep 17 00:00:00 2001 From: Andres Uribe Gonzalez Date: Wed, 26 Jul 2023 04:53:22 -0400 Subject: [PATCH 7/8] docs. --- doc/STORAGE.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/doc/STORAGE.md b/doc/STORAGE.md index ecffb3ecd..34420f9c4 100644 --- a/doc/STORAGE.md +++ b/doc/STORAGE.md @@ -75,7 +75,8 @@ which introduces the SQL based implementation. ## Encryption SSI Service supports application level encryption of values before sending them to the configured KV store. Please note -that keys (i.e. the key of the KV store) are not currently encrypted. A MasterKey is used (a.k.a. a Data Encryption Key or DEK). +that keys (i.e. the key of the KV store) are not currently encrypted. See the [Privacy Considerations](#privacy-considerations) for more information. +A MasterKey is used (a.k.a. a Data Encryption Key or DEK) to encrypt all data before it's sent to the configured storage. The MasterKey can be stored in the configured storage system or in an external Key Management System (KMS) like GCP KMS or AWS KMS. When storing locally, the key will be automatically generated if it doesn't exist already. @@ -111,4 +112,13 @@ Disabling app level encryption is also possible using the following options in y [services.storage_encryption] # encryption disable_encryption = true -``` \ No newline at end of file +``` + +### Privacy Considerations + +From the perspective of SSI-Service, all keys are stored in plaintext (this doesn't preclude configuring encryption at rest +in your deployment of the storage configuration). Making all keys readable by any actor may have an impact in your organization's +use cases around privacy. You should consider whether this is acceptable. Notably, a DID that was created by SSI Service +is stored as a key. This can fit some definition of PII, as it could be correlated to identify and individual. + +Encrypting keys is being considered in https://github.com/TBD54566975/ssi-service/issues/603. \ No newline at end of file From e7c94e625d180d8f5dab4b7d5fbda6f0eb50155d Mon Sep 17 00:00:00 2001 From: Andres Uribe Gonzalez Date: Wed, 26 Jul 2023 05:01:32 -0400 Subject: [PATCH 8/8] integration fix --- cmd/ssiservice/main.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cmd/ssiservice/main.go b/cmd/ssiservice/main.go index 9db1fc83a..560e07086 100644 --- a/cmd/ssiservice/main.go +++ b/cmd/ssiservice/main.go @@ -6,6 +6,7 @@ import ( "io" "os" "os/signal" + "path" "strconv" "syscall" "time" @@ -52,7 +53,9 @@ func run() error { logrus.Infof("loading config from env var path: %s", envConfigPath) configPath = envConfigPath } - cfg, err := config.LoadConfig(configPath, os.DirFS(".")) + + dir, file := path.Split(configPath) + cfg, err := config.LoadConfig(file, os.DirFS(dir)) if err != nil { logrus.Fatalf("could not instantiate config: %s", err.Error()) }