Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added app level encryption feature. #599

Merged
merged 9 commits into from
Jul 26, 2023

Conversation

andresuribe87
Copy link
Contributor

Overview

Allow for operators of ssi-service to choose whether to do app level encryption.

Description

At a high-level, this PR is adding a Data Encryption Key. The DEK can be stored either in the configured storage, or in a KMS. The DEK is used for every read/write that happens in the application.

Specifically, this PR does the following:

  • Removed some methods that weren't necessary in the storage interface.
  • Implemented and EncryptedWrapper, which takes in any implementation of a storage, and adds an encryption layer.
  • Refactored the existing Key Encryption Mechanism so it could be reused with the data encryption layer.
  • Added documentation.

How Has This Been Tested?

All unit tests and integration tests pass when encryption is enabled (see the dev.toml file).

@codecov-commenter
Copy link

codecov-commenter commented Jul 26, 2023

Codecov Report

Merging #599 (e7c94e6) into main (31ea6fd) will increase coverage by 1.27%.
The diff coverage is 50.51%.

@@            Coverage Diff             @@
##             main     #599      +/-   ##
==========================================
+ Coverage   25.70%   26.97%   +1.27%     
==========================================
  Files          46       48       +2     
  Lines        5699     5860     +161     
==========================================
+ Hits         1465     1581     +116     
- Misses       3951     3995      +44     
- Partials      283      284       +1     
Files Changed Coverage Δ
pkg/service/did/web.go 0.00% <0.00%> (ø)
pkg/storage/bolt.go 70.22% <ø> (+4.18%) ⬆️
pkg/encryption/encryption.go 16.17% <16.17%> (ø)
config/config.go 43.62% <42.85%> (+16.26%) ⬆️
pkg/service/keystore/storage.go 38.28% <42.85%> (+3.54%) ⬆️
pkg/service/operation/storage.go 25.25% <50.00%> (+0.51%) ⬆️
pkg/storage/encrypt.go 57.77% <57.77%> (ø)
pkg/service/keystore/service.go 42.75% <60.00%> (+0.64%) ⬆️
pkg/storage/storage.go 70.00% <85.48%> (+25.26%) ⬆️

... and 2 files with indirect coverage changes

Copy link
Member

@decentralgabe decentralgabe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would be great to have tests that exercise different confidence levels (no encryption, local encryption, hsm encryption)

for hsm we would need a reliable cloud test env, so maybe that's out of scope for now but would be good to track in a future issue.

similarly, we should track issues for managing key rotation.

if you didn't (I may have missed) it would be useful to highlight that we've specifically chosen an authenticated encryption scheme which gives us the added benefit of tamper evidence, and may also prove useful for decrypting/re-encrypting data during key rotations since we'll be able to see which key encrypted the data

another thing worth covering in the doc is the data that we need to keep in plaintext, and the considerations around that, for keys. these keys are often just UUIDs but sometimes contain other information to make prefix queries viable. that means they could contain a DID, which by someones definition may be PII. <-- not sure here but we should verify

}

type EncryptionConfig struct {
DisableEncryption bool `toml:"disable_encryption"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we have some circuit-breaker logic that doesn't allow this to be a set, or at least throws a warning if env == prod?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added some in a061870

@@ -72,6 +72,10 @@ type ServicesConfig struct {
StorageOptions []storage.Option `toml:"storage_option"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

general comment: it would be great to have a light design doc around this kind of thing before going for an implementation. there are a number of hairy details that would be good to discuss and get feedback on. not only that it would serve as a record for how the feature was thought through which will be useful in the future (as we have already seen with existing SIPs)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't deem it worth making a doc at the time I started this. Seemed pretty straightforward. I'll do one in a separate PR. Tracked in #602

doc/STORAGE.md Outdated
# encryption
disable_encryption = true
```
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is great to have

"context"
"strings"

util2 "github.com/TBD54566975/ssi-sdk/util"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
util2 "github.com/TBD54566975/ssi-sdk/util"
sdkutil "github.com/TBD54566975/ssi-sdk/util"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


type KeyResolver func(ctx context.Context) ([]byte, error)

type XChaCha20Poly1305Encrypter struct {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this only used locally since the doc above mentions only AES GCM is supported by HSMs?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's only used when the encryption keys are stored locally (either KEK or DEK).

@@ -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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer, that instead of a noopEncrypter and noopDecrypter we check config here, and if the flag is off we pass in nil values below, this avoids the overhead of a no-op call for each read/write

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIUC, I'm not quite sure there is any overhead.

A conditional statement evaluating whether an encrypter is nil is the same overhead as making a call that does a noop on a non-nil object. In fact, the latter might end up being optimized away by the compiler. Not having if statements typically helps with branch prediction.

I changed this internally so nil values can be returned, but I'm not convinced it's easier to read.

kt: crypto.RSA,
},
}
for _, test := range tests {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why delete this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The {En,De}cryptKey methods weren't being used, so I removed that test. After evaluating, the better thing to do is to make these tests acts on XChaCha20Poly1305Encrypter. I've moved this to the relevant file.

return nil
}

watchKeys := []storage.WatchKey{{
Namespace: namespace,
Key: skKey,
Key: encryptionKeyKey,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe encryptionKeyID?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Settled for encryptionMaterialKey.

if err != nil {
return nil, errors.Wrap(err, "creating app level encrypter")
}
storageProvider := storage.NewEncryptedWrapper(unencryptedStorageProvider, storageEncrypter, storageDecrypter)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if this is a no-op the encrypted wrapper is unnecessary

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

}
keyStoreServiceFactory := keystore.NewKeyStoreServiceFactory(config.KeyStoreConfig, storageProvider)
keyStoreServiceFactory := keystore.NewKeyStoreServiceFactory(config.KeyStoreConfig, storageProvider, keyEncrypter, keyDecrypter)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same comment here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed internally to handle possible nil key{En,De}crypter values.

@andresuribe87
Copy link
Contributor Author

it would be great to have tests that exercise different confidence levels (no encryption, local encryption, hsm encryption)

for hsm we would need a reliable cloud test env, so maybe that's out of scope for now but would be good to track in a future issue.

no encryption is covered in unit tests. Local encryption is covered in integration tests. HSM will be tracked in #600

similarly, we should track issues for managing key rotation.

#601

if you didn't (I may have missed) it would be useful to highlight that we've specifically chosen an authenticated encryption scheme which gives us the added benefit of tamper evidence, and may also prove useful for decrypting/re-encrypting data during key rotations since we'll be able to see which key encrypted the data

I'm going to leave that for the same issue as above (i.e. #601 )

another thing worth covering in the doc is the data that we need to keep in plaintext, and the considerations around that, for keys. these keys are often just UUIDs but sometimes contain other information to make prefix queries viable. that means they could contain a DID, which by someones definition may be PII. <-- not sure here but we should verify

We could encrypt keys as long as we preserve the key prefix structure. That said, it doesn't seem like it's worth the effort and additional complexity. I added some more info to the docs, and created another issue to track #603

Copy link
Contributor Author

@andresuribe87 andresuribe87 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like these never went through :O

}

type EncryptionConfig struct {
DisableEncryption bool `toml:"disable_encryption"`
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added some in a061870


type KeyResolver func(ctx context.Context) ([]byte, error)

type XChaCha20Poly1305Encrypter struct {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's only used when the encryption keys are stored locally (either KEK or DEK).

@@ -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)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIUC, I'm not quite sure there is any overhead.

A conditional statement evaluating whether an encrypter is nil is the same overhead as making a call that does a noop on a non-nil object. In fact, the latter might end up being optimized away by the compiler. Not having if statements typically helps with branch prediction.

I changed this internally so nil values can be returned, but I'm not convinced it's easier to read.

kt: crypto.RSA,
},
}
for _, test := range tests {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The {En,De}cryptKey methods weren't being used, so I removed that test. After evaluating, the better thing to do is to make these tests acts on XChaCha20Poly1305Encrypter. I've moved this to the relevant file.

return nil
}

watchKeys := []storage.WatchKey{{
Namespace: namespace,
Key: skKey,
Key: encryptionKeyKey,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Settled for encryptionMaterialKey.

if err != nil {
return nil, errors.Wrap(err, "creating app level encrypter")
}
storageProvider := storage.NewEncryptedWrapper(unencryptedStorageProvider, storageEncrypter, storageDecrypter)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

}
keyStoreServiceFactory := keystore.NewKeyStoreServiceFactory(config.KeyStoreConfig, storageProvider)
keyStoreServiceFactory := keystore.NewKeyStoreServiceFactory(config.KeyStoreConfig, storageProvider, keyEncrypter, keyDecrypter)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed internally to handle possible nil key{En,De}crypter values.

"context"
"strings"

util2 "github.com/TBD54566975/ssi-sdk/util"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@@ -72,6 +72,10 @@ type ServicesConfig struct {
StorageOptions []storage.Option `toml:"storage_option"`
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't deem it worth making a doc at the time I started this. Seemed pretty straightforward. I'll do one in a separate PR. Tracked in #602

@andresuribe87 andresuribe87 merged commit f836a81 into TBD54566975:main Jul 26, 2023
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants