Skip to content

Commit

Permalink
Added app level encryption feature.
Browse files Browse the repository at this point in the history
  • Loading branch information
andresuribe87 committed Jul 25, 2023
1 parent a0f4724 commit 7fdd5bf
Show file tree
Hide file tree
Showing 20 changed files with 590 additions and 305 deletions.
42 changes: 39 additions & 3 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand All @@ -94,21 +98,53 @@ 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
}
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"`
Expand Down
4 changes: 4 additions & 0 deletions config/dev.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
8 changes: 7 additions & 1 deletion config/prod.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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"

Expand Down
6 changes: 6 additions & 0 deletions config/test.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down
43 changes: 42 additions & 1 deletion doc/STORAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
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
```
164 changes: 164 additions & 0 deletions pkg/encryption/encryption.go
Original file line number Diff line number Diff line change
@@ -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
}
16 changes: 16 additions & 0 deletions pkg/server/router/did_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand All @@ -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})
Expand Down
6 changes: 3 additions & 3 deletions pkg/server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down
Loading

0 comments on commit 7fdd5bf

Please sign in to comment.