diff --git a/coordinator/clientapi/clientapi.go b/coordinator/clientapi/clientapi.go index de3fd8ed8..05c2d6eab 100644 --- a/coordinator/clientapi/clientapi.go +++ b/coordinator/clientapi/clientapi.go @@ -28,6 +28,7 @@ import ( "github.com/edgelesssys/marblerun/coordinator/manifest" "github.com/edgelesssys/marblerun/coordinator/quote" "github.com/edgelesssys/marblerun/coordinator/recovery" + "github.com/edgelesssys/marblerun/coordinator/seal" "github.com/edgelesssys/marblerun/coordinator/state" "github.com/edgelesssys/marblerun/coordinator/store" "github.com/edgelesssys/marblerun/coordinator/store/request" @@ -57,7 +58,7 @@ type core interface { type transactionHandle interface { BeginTransaction(context.Context) (store.Transaction, error) - SetEncryptionKey([]byte) error + SetEncryptionKey([]byte, seal.Mode) error SetRecoveryData([]byte) LoadState() ([]byte, error) } @@ -298,7 +299,7 @@ func (a *ClientAPI) Recover(ctx context.Context, encryptionKey []byte) (keysLeft } // all keys are set, we can now load the state - if err := a.txHandle.SetEncryptionKey(secret); err != nil { + if err := a.txHandle.SetEncryptionKey(secret, seal.ModeDisabled); err != nil { return -1, fmt.Errorf("setting recovery key: %w", err) } @@ -319,6 +320,15 @@ func (a *ClientAPI) Recover(ctx context.Context, encryptionKey []byte) (keysLeft } defer rollback() + // set seal mode defined in manifest + mnf, err := txdata.GetManifest() + if err != nil { + return -1, fmt.Errorf("loading manifest from store: %w", err) + } + if err := a.txHandle.SetEncryptionKey(secret, seal.ModeFromString(mnf.Config.SealMode)); err != nil { + return -1, fmt.Errorf("setting recovery key and seal mode: %w", err) + } + rootCert, err := txdata.GetCertificate(constants.SKCoordinatorRootCert) if err != nil { return -1, fmt.Errorf("loading root certificate from store: %w", err) @@ -400,7 +410,7 @@ func (a *ClientAPI) SetManifest(ctx context.Context, rawManifest []byte) (recove a.log.Error("could not generate recovery data", zap.Error(err)) return nil, fmt.Errorf("generating recovery data: %w", err) } - if err := a.txHandle.SetEncryptionKey(encryptionKey); err != nil { + if err := a.txHandle.SetEncryptionKey(encryptionKey, seal.ModeFromString(mnf.Config.SealMode)); err != nil { a.log.Error("could not set encryption key to seal state", zap.Error(err)) return nil, fmt.Errorf("setting encryption key: %w", err) } @@ -817,7 +827,7 @@ func (a *ClientAPI) FeatureEnabled(ctx context.Context, feature string) bool { return false } - return slices.ContainsFunc(mnf.FeatureGates, func(s string) bool { + return slices.ContainsFunc(mnf.Config.FeatureGates, func(s string) bool { return strings.EqualFold(s, feature) }) } diff --git a/coordinator/clientapi/clientapi_test.go b/coordinator/clientapi/clientapi_test.go index 084721869..c00a4e3f1 100644 --- a/coordinator/clientapi/clientapi_test.go +++ b/coordinator/clientapi/clientapi_test.go @@ -404,7 +404,9 @@ func TestRecover(t *testing.T) { _, rootCert := test.MustSetupTestCerts(test.RecoveryPrivateKey) defaultStore := func() store.Store { s := stdstore.New(&seal.MockSealer{}, afero.NewMemMapFs(), "") - require.NoError(t, wrapper.New(s).PutCertificate(constants.SKCoordinatorRootCert, rootCert)) + wr := wrapper.New(s) + require.NoError(t, wr.PutCertificate(constants.SKCoordinatorRootCert, rootCert)) + require.NoError(t, wr.PutRawManifest([]byte(`{}`))) return s } @@ -548,10 +550,11 @@ func TestRecover(t *testing.T) { func TestSetManifest(t *testing.T) { testCases := map[string]struct { - store *fakeStoreTransaction - core *fakeCore - manifest []byte - wantErr bool + store *fakeStoreTransaction + core *fakeCore + manifest []byte + wantErr bool + wantSealMode seal.Mode }{ "success": { store: &fakeStoreTransaction{ @@ -560,7 +563,76 @@ func TestSetManifest(t *testing.T) { core: &fakeCore{ state: state.AcceptingManifest, }, - manifest: []byte(test.ManifestJSON), + manifest: []byte(test.ManifestJSON), + wantSealMode: seal.ModeProductKey, + }, + "seal mode set to product key": { + store: &fakeStoreTransaction{ + state: make(map[string][]byte), + }, + core: &fakeCore{ + state: state.AcceptingManifest, + }, + manifest: func() []byte { + var mnf manifest.Manifest + require.NoError(t, json.Unmarshal([]byte(test.ManifestJSON), &mnf)) + mnf.Config.SealMode = "ProductKey" + mnfBytes, err := json.Marshal(mnf) + require.NoError(t, err) + return mnfBytes + }(), + wantSealMode: seal.ModeProductKey, + }, + "seal mode set to unique key": { + store: &fakeStoreTransaction{ + state: make(map[string][]byte), + }, + core: &fakeCore{ + state: state.AcceptingManifest, + }, + manifest: func() []byte { + var mnf manifest.Manifest + require.NoError(t, json.Unmarshal([]byte(test.ManifestJSON), &mnf)) + mnf.Config.SealMode = "UniqueKey" + mnfBytes, err := json.Marshal(mnf) + require.NoError(t, err) + return mnfBytes + }(), + wantSealMode: seal.ModeUniqueKey, + }, + "seal mode set to disabled": { + store: &fakeStoreTransaction{ + state: make(map[string][]byte), + }, + core: &fakeCore{ + state: state.AcceptingManifest, + }, + manifest: func() []byte { + var mnf manifest.Manifest + require.NoError(t, json.Unmarshal([]byte(test.ManifestJSON), &mnf)) + mnf.Config.SealMode = "Disabled" + mnfBytes, err := json.Marshal(mnf) + require.NoError(t, err) + return mnfBytes + }(), + wantSealMode: seal.ModeDisabled, + }, + "invalid seal mode": { + store: &fakeStoreTransaction{ + state: make(map[string][]byte), + }, + core: &fakeCore{ + state: state.AcceptingManifest, + }, + manifest: func() []byte { + var mnf manifest.Manifest + require.NoError(t, json.Unmarshal([]byte(test.ManifestJSON), &mnf)) + mnf.Config.SealMode = "foo" + mnfBytes, err := json.Marshal(mnf) + require.NoError(t, err) + return mnfBytes + }(), + wantErr: true, }, "wrong state": { store: &fakeStoreTransaction{ @@ -629,6 +701,7 @@ func TestSetManifest(t *testing.T) { require.NoError(err) assert.True(tc.core.unlockCalled) assert.True(tc.store.commitCalled) + assert.Equal(tc.wantSealMode, tc.store.sealMode) }) } } @@ -729,7 +802,7 @@ func TestFeatureEnabled(t *testing.T) { request.Manifest: func() []byte { var mnf manifest.Manifest require.NoError(t, json.Unmarshal([]byte(test.ManifestJSON), &mnf)) - mnf.FeatureGates = []string{} + mnf.Config.FeatureGates = []string{} mnfBytes, err := json.Marshal(mnf) require.NoError(t, err) return mnfBytes @@ -899,7 +972,7 @@ func (s *fakeStore) BeginTransaction(ctx context.Context) (store.Transaction, er return s.store.BeginTransaction(ctx) } -func (s *fakeStore) SetEncryptionKey(key []byte) error { +func (s *fakeStore) SetEncryptionKey(key []byte, _ seal.Mode) error { if s.setEncryptionKeyErr != nil { return s.setEncryptionKeyErr } @@ -965,6 +1038,7 @@ type fakeStoreTransaction struct { beginTransactionErr error setEncryptionKeyCalled bool setEncryptionKeyErr error + sealMode seal.Mode loadStateCalled bool loadStateErr error setRecoveryDataCalled bool @@ -984,8 +1058,9 @@ func (s *fakeStoreTransaction) BeginTransaction(_ context.Context) (store.Transa return s, s.beginTransactionErr } -func (s *fakeStoreTransaction) SetEncryptionKey(_ []byte) error { +func (s *fakeStoreTransaction) SetEncryptionKey(_ []byte, mode seal.Mode) error { s.setEncryptionKeyCalled = true + s.sealMode = mode return s.setEncryptionKeyErr } diff --git a/coordinator/core/core.go b/coordinator/core/core.go index fe7103201..fd3cf69ae 100644 --- a/coordinator/core/core.go +++ b/coordinator/core/core.go @@ -601,7 +601,7 @@ func (e QuoteError) Error() string { type transactionHandle interface { BeginTransaction(context.Context) (store.Transaction, error) - SetEncryptionKey([]byte) error + SetEncryptionKey([]byte, seal.Mode) error SetRecoveryData([]byte) LoadState() ([]byte, error) } diff --git a/coordinator/manifest/manifest.go b/coordinator/manifest/manifest.go index 7dc230fca..860c7b9a1 100644 --- a/coordinator/manifest/manifest.go +++ b/coordinator/manifest/manifest.go @@ -61,6 +61,14 @@ type Manifest struct { Roles map[string]Role // TLS contains tags which can be assigned to Marbles to specify which connections should be elevated to TLS TLS map[string]TLStag + // Config contains optional configuration for the Coordinator. + Config Config +} + +// Config contains optional configuration for the Coordinator. +type Config struct { + // SealMode specifies how the data should be sealed. Can be "ProductKey" (default if empty), "UniqueKey", or "Disabled". + SealMode string // FeatureGates is a list of additional features to enable on the Coordinator. FeatureGates []string } @@ -493,7 +501,13 @@ func (m Manifest) Check(zaplogger *zap.Logger) error { } } - for _, feature := range m.FeatureGates { + switch m.Config.SealMode { + case "", "ProductKey", "UniqueKey", "Disabled": + default: + return fmt.Errorf("unknown seal mode: %s", m.Config.SealMode) + } + + for _, feature := range m.Config.FeatureGates { if feature != FeatureSignQuoteEndpoint { return fmt.Errorf("unknown feature gate: %s", feature) } diff --git a/coordinator/seal/mocksealer.go b/coordinator/seal/mocksealer.go index 9becfbfbc..1117f84b1 100644 --- a/coordinator/seal/mocksealer.go +++ b/coordinator/seal/mocksealer.go @@ -28,8 +28,11 @@ func (s *MockSealer) Seal(unencryptedData []byte, toBeEncrypted []byte) ([]byte, // SealEncryptionKey implements the Sealer interface. // Since the MockSealer does not support sealing with an enclave key, it returns the key as is. -func (s *MockSealer) SealEncryptionKey(key []byte) ([]byte, error) { - return key, nil +func (s *MockSealer) SealEncryptionKey(key []byte, mode Mode) ([]byte, error) { + if mode == ModeProductKey || mode == ModeUniqueKey { + return key, nil + } + panic("invariant not met: unexpected mode") } // SetEncryptionKey implements the Sealer interface. diff --git a/coordinator/seal/mode.go b/coordinator/seal/mode.go new file mode 100644 index 000000000..9bbd0fcfa --- /dev/null +++ b/coordinator/seal/mode.go @@ -0,0 +1,32 @@ +// Copyright (c) Edgeless Systems GmbH. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +package seal + +import "strings" + +// Mode specifies how the data should be sealed. +type Mode uint + +const ( + // ModeDisabled disables sealing and holds data in memory only. + ModeDisabled Mode = iota + // ModeProductKey enables sealing with the product key. + ModeProductKey + // ModeUniqueKey enables sealing with the unique key. + ModeUniqueKey +) + +// ModeFromString returns the Mode value for the given string. +func ModeFromString(mode string) Mode { + switch { + case mode == "", strings.EqualFold(mode, "ProductKey"): + return ModeProductKey + case strings.EqualFold(mode, "UniqueKey"): + return ModeUniqueKey + } + return ModeDisabled +} diff --git a/coordinator/seal/noenclavesealer.go b/coordinator/seal/noenclavesealer.go index 1c84d6174..041de05f9 100644 --- a/coordinator/seal/noenclavesealer.go +++ b/coordinator/seal/noenclavesealer.go @@ -50,7 +50,7 @@ func (s *NoEnclaveSealer) Seal(unencryptedData []byte, toBeEncrypted []byte) ([] // SealEncryptionKey implements the Sealer interface. // Since the NoEnclaveSealer does not support sealing with an enclave key, it returns the key as is. -func (s *NoEnclaveSealer) SealEncryptionKey(key []byte) ([]byte, error) { +func (s *NoEnclaveSealer) SealEncryptionKey(key []byte, _ Mode) ([]byte, error) { return key, nil } diff --git a/coordinator/seal/seal.go b/coordinator/seal/seal.go index a93064e85..c19617c9f 100644 --- a/coordinator/seal/seal.go +++ b/coordinator/seal/seal.go @@ -39,7 +39,7 @@ type Sealer interface { // Unseal decrypts the given data and returns the plain text, as well as the unencrypted metadata. Unseal(encryptedData []byte) (unencryptedData []byte, decryptedData []byte, err error) // SealEncryptionKey seals an encryption key using the sealer. - SealEncryptionKey(key []byte) (encryptedKey []byte, err error) + SealEncryptionKey(key []byte, mode Mode) (encryptedKey []byte, err error) // SetEncryptionKey sets the encryption key of the sealer. SetEncryptionKey(key []byte) // UnsealEncryptionKey decrypts an encrypted key. @@ -82,15 +82,15 @@ func (s *AESGCMSealer) Seal(unencryptedData []byte, toBeEncrypted []byte) ([]byt return sealData(unencryptedData, toBeEncrypted, s.encryptionKey) } -// SealEncryptionKey seals an encryption key with the enclave's product key. -func (s *AESGCMSealer) SealEncryptionKey(encryptionKey []byte) ([]byte, error) { - // Encrypt encryption key with seal key - encryptedKeyData, err := ecrypto.SealWithProductKey(encryptionKey, nil) - if err != nil { - return nil, err +// SealEncryptionKey seals an encryption key with the selected enclave key. +func (s *AESGCMSealer) SealEncryptionKey(encryptionKey []byte, mode Mode) ([]byte, error) { + switch mode { + case ModeProductKey: + return ecrypto.SealWithProductKey(encryptionKey, nil) + case ModeUniqueKey: + return ecrypto.SealWithUniqueKey(encryptionKey, nil) } - - return encryptedKeyData, nil + return nil, errors.New("sealing is disabled") } // SetEncryptionKey sets the encryption key of the Sealer. @@ -98,7 +98,7 @@ func (s *AESGCMSealer) SetEncryptionKey(encryptionKey []byte) { s.encryptionKey = encryptionKey } -// UnsealEncryptionKey unseals the encryption key using the enclave's product key. +// UnsealEncryptionKey unseals the encryption key. func (s *AESGCMSealer) UnsealEncryptionKey(encryptedKey []byte) ([]byte, error) { // Decrypt stored encryption key with seal key encryptionKey, err := ecrypto.Unseal(encryptedKey, nil) diff --git a/coordinator/store/stdstore/stdstore.go b/coordinator/store/stdstore/stdstore.go index 00a7141d3..0bbb33599 100644 --- a/coordinator/store/stdstore/stdstore.go +++ b/coordinator/store/stdstore/stdstore.go @@ -34,6 +34,7 @@ type StdStore struct { data map[string][]byte mux, txmux sync.Mutex sealer seal.Sealer + sealMode seal.Mode fs afero.Afero recoveryData []byte @@ -158,21 +159,24 @@ func (s *StdStore) SetRecoveryData(recoveryData []byte) { } // SetEncryptionKey sets the encryption key for sealing and unsealing. -func (s *StdStore) SetEncryptionKey(encryptionKey []byte) error { - // If there already is an existing key file stored on disk, save it - s.backupEncryptionKey() +func (s *StdStore) SetEncryptionKey(encryptionKey []byte, mode seal.Mode) error { + if mode != seal.ModeDisabled { + // If there already is an existing key file stored on disk, save it + s.backupEncryptionKey() - encryptedKey, err := s.sealer.SealEncryptionKey(encryptionKey) - if err != nil { - return fmt.Errorf("encrypting data key: %w", err) - } + encryptedKey, err := s.sealer.SealEncryptionKey(encryptionKey, mode) + if err != nil { + return fmt.Errorf("encrypting data key: %w", err) + } - // Write the sealed encryption key to disk - if err = s.fs.WriteFile(filepath.Join(s.sealDir, SealedKeyFname), encryptedKey, 0o600); err != nil { - return fmt.Errorf("writing encrypted key to disk: %w", err) + // Write the sealed encryption key to disk + if err = s.fs.WriteFile(filepath.Join(s.sealDir, SealedKeyFname), encryptedKey, 0o600); err != nil { + return fmt.Errorf("writing encrypted key to disk: %w", err) + } } s.sealer.SetEncryptionKey(encryptionKey) + s.sealMode = mode return nil } @@ -199,35 +203,10 @@ func (s *StdStore) commit(data map[string][]byte) error { s.mux.Lock() defer s.mux.Unlock() - if !s.recoveryMode { + if !s.recoveryMode && s.sealMode != seal.ModeDisabled { sealedData, err := s.sealer.Seal(s.recoveryData, dataRaw) if err != nil { - if !errors.Is(err, seal.ErrMissingEncryptionKey) { - return err - } - - // No encryption key set - // Load or generate new key, and retry sealing - if err := s.unsealEncryptionKey(); err != nil { - if !errors.Is(err, afero.ErrFileNotFound) { - return err - } - - // No encryption key on disk - // Generate a new encryption key, and seal it with product key - encryptionKey, err := seal.GenerateEncryptionKey() - if err != nil { - return err - } - if err := s.SetEncryptionKey(encryptionKey); err != nil { - return err - } - } - - sealedData, err = s.sealer.Seal(s.recoveryData, dataRaw) - if err != nil { - return err - } + return err } if err := s.fs.WriteFile(filepath.Join(s.sealDir, SealedDataFname), sealedData, 0o600); err != nil { diff --git a/coordinator/store/stdstore/stdstore_test.go b/coordinator/store/stdstore/stdstore_test.go index a5f48c2f6..796667dd7 100644 --- a/coordinator/store/stdstore/stdstore_test.go +++ b/coordinator/store/stdstore/stdstore_test.go @@ -14,6 +14,7 @@ import ( "github.com/edgelesssys/marblerun/coordinator/store" "github.com/spf13/afero" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestStdStore(t *testing.T) { @@ -105,25 +106,46 @@ func TestStdIterator(t *testing.T) { } func TestStdStoreSealing(t *testing.T) { - assert := assert.New(t) + testCases := map[string]struct { + mode seal.Mode + wantErr bool + }{ + "product key": {mode: seal.ModeProductKey}, + "unique key": {mode: seal.ModeUniqueKey}, + "disabled": {mode: seal.ModeDisabled, wantErr: true}, + } - fs := afero.NewMemMapFs() - sealer := &seal.MockSealer{} + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) - store := New(sealer, fs, "") - _, err := store.LoadState() - assert.NoError(err) + fs := afero.NewMemMapFs() + sealer := &seal.MockSealer{} - testData1 := []byte("test data") - assert.NoError(store.Put("test:input", testData1)) + store := New(sealer, fs, "") + _, err := store.LoadState() + require.NoError(err) - // Check sealing with a new store initialized with the sealed state - store2 := New(sealer, fs, "") - _, err = store2.LoadState() - assert.NoError(err) - val, err := store2.Get("test:input") - assert.NoError(err) - assert.Equal(testData1, val) + require.NoError(store.SetEncryptionKey(nil, tc.mode)) + + testData1 := []byte("test data") + require.NoError(store.Put("test:input", testData1)) + + // Check sealing with a new store initialized with the sealed state + store2 := New(sealer, fs, "") + _, err = store2.LoadState() + require.NoError(err) + val, err := store2.Get("test:input") + + if tc.wantErr { + assert.Error(err) + return + } + require.NoError(err) + assert.Equal(testData1, val) + }) + } } func TestStdStoreRollback(t *testing.T) { diff --git a/coordinator/store/store.go b/coordinator/store/store.go index a4d2435de..f98f21ecf 100644 --- a/coordinator/store/store.go +++ b/coordinator/store/store.go @@ -9,6 +9,8 @@ package store import ( "context" "errors" + + "github.com/edgelesssys/marblerun/coordinator/seal" ) // Store is the interface for persistence. @@ -16,7 +18,7 @@ type Store interface { // BeginTransaction starts a new transaction. BeginTransaction(context.Context) (Transaction, error) // SetEncryptionKey sets the encryption key for the store. - SetEncryptionKey([]byte) error + SetEncryptionKey([]byte, seal.Mode) error // SetRecoveryData sets recovery data for the store. SetRecoveryData([]byte) // LoadState loads the sealed state of a store. diff --git a/docs/docs/_media/enc-state-distributed.svg b/docs/docs/_media/enc-state-distributed.svg index 646659e43..1c644caa4 100644 --- a/docs/docs/_media/enc-state-distributed.svg +++ b/docs/docs/_media/enc-state-distributed.svg @@ -1,3 +1,3 @@ -
Kubernetes Secret
Kubernetes Secret
Encrypted State
Encrypted State
Encrypted DEK
Encrypted DEK
Coordinator State
Coordinator State
Data Encryption Key
Data Encryption K...
Key Encryption Key
Key Encryption Key
Generated by the first Coordinator
Generated by the fir...
Generated by the first Coordinator
Generated by the fir...
Kubernetes ConfigMap
Kubernetes ConfigMap
Node-1: enc(KEK)
Node-1: enc(KEK)
Node-2: enc(KEK)
Node-2: enc(KEK)
Node-X: enc(KEK)
Node-X: enc(KEK)
Node-1 SGX 'Product' Sealing Key
Node-1 SGX 'Product' Sealing Key
Node-2 SGX 'Product' Sealing Key
Node-2 SGX 'Product' Sealing Key
Node-X SGX 'Product' Sealing Key
Node-X SGX 'Product' Sealing Key
...
...
...
...
Text is not SVG - cannot display
\ No newline at end of file +
Kubernetes secret
Kubernetes secret
Encrypted state
Encrypted state
Encrypted DEK
Encrypted DEK
Coordinator state
Coordinator state
Data encryption key
Data Encryption K...
Key encryption key
Key encryption key
Generated by the first Coordinator
Generated by the fir...
Generated by the first Coordinator
Generated by the fir...
Kubernetes ConfigMap
Kubernetes ConfigMap
Node-1: enc(KEK)
Node-1: enc(KEK)
Node-2: enc(KEK)
Node-2: enc(KEK)
Node-X: enc(KEK)
Node-X: enc(KEK)
Node-1 SGX seal key
Node-1 SGX seal key
Node-2 SGX seal key
Node-2 SGX seal key
Node-X SGX seal key
Node-X SGX seal key
...
...
...
...
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/docs/_media/enc-state-single.svg b/docs/docs/_media/enc-state-single.svg index 50df40aaa..34c5b8477 100644 --- a/docs/docs/_media/enc-state-single.svg +++ b/docs/docs/_media/enc-state-single.svg @@ -1,3 +1,3 @@ -
Persistent Storage
Persistent Storage
Encrypted State
Encrypted State
Encrypted DEK
Encrypted DEK
Coordinator State
Coordinator State
Data Encryption Key
Data Encryption K...
Key Encryption Key
Key Encryption Key
Generated by the Coordinator
Generated by the Coordi...
SGX 'Product' Sealing Key
SGX 'Product' Sealing K...
Text is not SVG - cannot display
\ No newline at end of file +
Persistent storage
Persistent storage
Encrypted state
Encrypted state
Encrypted DEK
Encrypted DEK
Coordinator state
Coordinator state
Data encryption key
Data Encryption K...
Key encryption key
Key encryption key
Generated by the Coordinator
Generated by the Coordi...
SGX seal key
SGX seal key
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/docs/architecture/security.md b/docs/docs/architecture/security.md index 670241e19..18b04e815 100644 --- a/docs/docs/architecture/security.md +++ b/docs/docs/architecture/security.md @@ -77,16 +77,13 @@ The protocol can be used by clients to verify a server certificate, by a server The Coordinator holds MarbleRun's state, which consists of the [manifest](../features/manifest.md), the [managed secrets](../features/secrets-management.md), and the [certificates for its CA](../features/attestation.md). The state is stored encrypted in persistent storage. For this, MarbleRun uses [AES128-GCM](https://www.rfc-editor.org/rfc/rfc5116#section-5.1) and a generated 16-byte data encryption key (DEK). -The DEK is also sealed to persistent storage to recover the state in case of a restart autonomously. -[SGX sealing](https://www.intel.com/content/www/us/en/developer/articles/technical/introduction-to-intel-sgx-sealing.html) is used for that purpose. The Coordinator encrypts the DEK with a key encryption key (KEK). -MarbleRun uses the SGX sealing key called `Product key` as its KEK, which is bound to its `Product ID` and the enclave's author `MRSIGNER` identity. -In other words, a fresh and benign enclave instance of the same identity can recover that key. -Hence, if the Coordinator is restarted on the same CPU, it can obtain the same KEK from the CPU, decrypt the DEK, and recover its state. +MarbleRun uses an [SGX seal key](#seal-key) as its KEK. +Hence, if the Coordinator is restarted on the same CPU, it can obtain the same KEK from the CPU, decrypt the DEK, and recover its state autonomously. ![Encrypted state single instance](../_media/enc-state-single.svg) -If the Coordinator is restarted on a different CPU, it won't be able to obtain the same SGX sealing key from the CPU. +If the Coordinator is restarted on a different CPU, it won't be able to obtain the same SGX seal key from the CPU. To address this, MarbleRun provides a [recovery feature](../features/recovery.md#recovery). The manifest allows for specifying a designated Recovery Key. The Recovery Key is a RSA public key. Upon startup, the Coordinator encrypts the DEK with this public key and returns it to the user. In case of a recovery event, the user decrypts the DEK locally and [uploads it to the Coordinator](../workflows/recover-coordinator.md). @@ -102,6 +99,17 @@ During a recovery event, every party will upload their share of the secret, whic The [distributed Coordinator](../features/recovery.md#distributed-coordinator) works similarly. However, all Coordinators share the same state stored encrypted in the Kubernetes [Secret](https://kubernetes.io/docs/concepts/configuration/secret/) called *marblerun-state*. In contrast to the single instance, the KEK is generated at start-up by the first instance. The existing Coordinators authenticate every new Coordinator instance via remote attestation, and the KEK is subsequently shared via the secure and attested TLS connection. -Every Coordinator instance uses its own SGX Product (Sealing) Key to seal the KEK into a Kubernetes [ConfigMap](https://kubernetes.io/docs/concepts/configuration/configmap/) called *marblerun-sealed-kek*. +Every Coordinator instance uses an SGX seal key to seal the KEK into a Kubernetes [ConfigMap](https://kubernetes.io/docs/concepts/configuration/configmap/) called *marblerun-sealed-kek*. ![Encrypted state distributed](../_media/enc-state-distributed.svg) + +### Seal key + +There are two types of [SGX seal keys](https://www.intel.com/content/www/us/en/developer/articles/technical/introduction-to-intel-sgx-sealing.html): + +* Unique key: This key is derived from the UniqueID (MRENCLAVE) of the enclave. This means that only instances of the same enclave can derive this key. +* Product key: This key is derived from the SignerID (MRSIGNER), ProductID, and SecurityVersion of the enclave. If the signer of the enclave creates a new enclave with the same ProductID and the same or higher SecurityVersion, that enclave can derive the same key. + +By default, MarbleRun uses the product key as the KEK. +This allows the Coordinator to be updated without manual recovery. +If you don't want to trust the signing entity, you can [enable sealing with the unique key](../workflows/define-manifest.md#config). diff --git a/docs/docs/workflows/define-manifest.md b/docs/docs/workflows/define-manifest.md index 52ffb5bfd..6029e3509 100644 --- a/docs/docs/workflows/define-manifest.md +++ b/docs/docs/workflows/define-manifest.md @@ -1,6 +1,6 @@ # Defining a manifest -The manifest is a simple JSON file that determines the key properties of your cluster: [`Packages`](#packages), [`Marbles`](#marbles), [`Secrets`](#secrets), [`Users`](#users), [`Roles`](#roles), [`RecoveryKeys`](#recoverykeys), and [`TLS`](#tls). +The manifest is a simple JSON file that determines the key properties of your cluster: [`Packages`](#packages), [`Marbles`](#marbles), [`Secrets`](#secrets), [`Users`](#users), [`Roles`](#roles), [`RecoveryKeys`](#recoverykeys), [`TLS`](#tls), and [`Config`](#config). This article describes how to define these in your `manifest.json`. For a working example see the manifest of the [emojivoto demo](https://github.com/edgelesssys/emojivoto/blob/main/tools/manifest.json). See also the [sample and template manifests](https://github.com/edgelesssys/marblerun/tree/master/samples). @@ -505,3 +505,31 @@ On startup, a Marble logs its effective TTLS policy. This helps to verify that the manifest configuration is applied as intended. ::: + +## Config + +The optional entry `Config` holds configuration settings for the Coordinator. + +```javascript +{ + //... + "Config": + { + "SealMode": "ProductKey", + "FeatureGates": [] + } + //... +} +``` + +`SealMode` lets you specify how the Coordinator should seal its state. The following options are available: + +* `ProductKey`: Sealing uses the product key. This is the default if not set. +* `UniqueKey`: Sealing uses the unique key. +* `Disabled`: In single instance mode, the Coordinator won't persist state. This can be useful for ephemeral deployments. For distributed Coordinator mode, this setting is the same as `UniqueKey`. + +See the section on [seal key types](../architecture/security.md#seal-key) for more information. + +`FeatureGates` allows you to opt-in to additional features that may be useful for certain use cases. The following features are available: + +* `SignQuoteEndpoint`: enables the [sign-quote endpoint](../reference/coordinator.md#verify-and-sign-an-sgx-quote) diff --git a/test/integration_test.go b/test/integration_test.go index 0ae8a096f..a29852b76 100644 --- a/test/integration_test.go +++ b/test/integration_test.go @@ -83,39 +83,44 @@ func TestTest(t *testing.T) { } func TestMarbleAPI(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - f := newFramework(t) - - // start Coordinator - t.Log("Starting a coordinator enclave") - cfg := framework.NewCoordinatorConfig() - defer cfg.Cleanup() - f.StartCoordinator(f.Ctx, cfg) - - // set Manifest - t.Log("Setting the Manifest") - _, err := f.SetManifest(f.TestManifest) - require.NoError(err, "failed to set Manifest") - - // start server - t.Log("Starting a Server-Marble...") - serverCfg := framework.NewMarbleConfig(meshServerAddr, "testMarbleServer", "server,backend,localhost") - defer serverCfg.Cleanup() - f.StartMarbleServer(f.Ctx, serverCfg) - - // start clients - t.Log("Starting a bunch of Client-Marbles...") - clientCfg := framework.NewMarbleConfig(meshServerAddr, "testMarbleClient", "client,frontend,localhost") - defer clientCfg.Cleanup() - assert.True(f.StartMarbleClient(f.Ctx, clientCfg)) - assert.True(f.StartMarbleClient(f.Ctx, clientCfg)) - if !*simulationMode && !*noenclave { - // start bad marbles (would be accepted if we run in SimulationMode) - badCfg := framework.NewMarbleConfig(meshServerAddr, "badMarble", "bad,localhost") - defer badCfg.Cleanup() - assert.False(f.StartMarbleClient(f.Ctx, badCfg)) - assert.False(f.StartMarbleClient(f.Ctx, badCfg)) + for _, sealMode := range []string{"", "Disabled", "ProductKey", "UniqueKey"} { + t.Run("SealMode="+sealMode, func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + f := newFramework(t) + + // start Coordinator + t.Log("Starting a coordinator enclave") + cfg := framework.NewCoordinatorConfig() + defer cfg.Cleanup() + f.StartCoordinator(f.Ctx, cfg) + + // set Manifest + t.Log("Setting the Manifest") + f.TestManifest.Config.SealMode = sealMode + _, err := f.SetManifest(f.TestManifest) + require.NoError(err, "failed to set Manifest") + + // start server + t.Log("Starting a Server-Marble...") + serverCfg := framework.NewMarbleConfig(meshServerAddr, "testMarbleServer", "server,backend,localhost") + defer serverCfg.Cleanup() + f.StartMarbleServer(f.Ctx, serverCfg) + + // start clients + t.Log("Starting a bunch of Client-Marbles...") + clientCfg := framework.NewMarbleConfig(meshServerAddr, "testMarbleClient", "client,frontend,localhost") + defer clientCfg.Cleanup() + assert.True(f.StartMarbleClient(f.Ctx, clientCfg)) + assert.True(f.StartMarbleClient(f.Ctx, clientCfg)) + if !*simulationMode && !*noenclave { + // start bad marbles (would be accepted if we run in SimulationMode) + badCfg := framework.NewMarbleConfig(meshServerAddr, "badMarble", "bad,localhost") + defer badCfg.Cleanup() + assert.False(f.StartMarbleClient(f.Ctx, badCfg)) + assert.False(f.StartMarbleClient(f.Ctx, badCfg)) + } + }) } } @@ -287,46 +292,51 @@ func TestSettingSecrets(t *testing.T) { } func TestRecoveryRestoreKey(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - f := newFramework(t) - - t.Log("Testing recovery...") - t.Log("Starting a coordinator enclave") - cfg := framework.NewCoordinatorConfig() - defer cfg.Cleanup() - cancelCoordinator := f.StartCoordinator(f.Ctx, cfg) - - // set Manifest - t.Log("Setting the Manifest") - recoveryResponse, err := f.SetManifest(f.TestManifest) - require.NoError(err, "failed to set Manifest") - - // start server - t.Log("Starting a Server-Marble") - serverCfg := framework.NewMarbleConfig(f.MeshServerAddr, "testMarbleServer", "server,backend,localhost") - defer serverCfg.Cleanup() - f.StartMarbleServer(f.Ctx, serverCfg) - - // Trigger recovery mode - cancelCoordinator, cert := f.TriggerRecovery(cfg, cancelCoordinator) - - // Decode & Decrypt recovery data from when we set the manifest - key := gjson.GetBytes(recoveryResponse, "data.RecoverySecrets.testRecKey1").String() - recoveryDataEncrypted, err := base64.StdEncoding.DecodeString(key) - require.NoError(err, "Failed to base64 decode recovery data.") - recoveryKey, err := util.DecryptOAEP(RecoveryPrivateKey, recoveryDataEncrypted) - require.NoError(err, "Failed to RSA OAEP decrypt the recovery data.") - - // Perform recovery - require.NoError(f.SetRecover(recoveryKey)) - t.Log("Performed recovery, now checking status again...") - statusResponse, err := f.GetStatus() - require.NoError(err) - assert.EqualValues(3, gjson.Get(statusResponse, "data.StatusCode").Int(), "Server is in wrong status after recovery.") - - // Verify if old certificate is still valid - f.VerifyCertAfterRecovery(cert, cancelCoordinator, cfg) + for _, sealMode := range []string{"", "ProductKey", "UniqueKey"} { + t.Run("SealMode="+sealMode, func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + f := newFramework(t) + + t.Log("Testing recovery...") + t.Log("Starting a coordinator enclave") + cfg := framework.NewCoordinatorConfig() + defer cfg.Cleanup() + cancelCoordinator := f.StartCoordinator(f.Ctx, cfg) + + // set Manifest + t.Log("Setting the Manifest") + f.TestManifest.Config.SealMode = sealMode + recoveryResponse, err := f.SetManifest(f.TestManifest) + require.NoError(err, "failed to set Manifest") + + // start server + t.Log("Starting a Server-Marble") + serverCfg := framework.NewMarbleConfig(f.MeshServerAddr, "testMarbleServer", "server,backend,localhost") + defer serverCfg.Cleanup() + f.StartMarbleServer(f.Ctx, serverCfg) + + // Trigger recovery mode + cancelCoordinator, cert := f.TriggerRecovery(cfg, cancelCoordinator) + + // Decode & Decrypt recovery data from when we set the manifest + key := gjson.GetBytes(recoveryResponse, "data.RecoverySecrets.testRecKey1").String() + recoveryDataEncrypted, err := base64.StdEncoding.DecodeString(key) + require.NoError(err, "Failed to base64 decode recovery data.") + recoveryKey, err := util.DecryptOAEP(RecoveryPrivateKey, recoveryDataEncrypted) + require.NoError(err, "Failed to RSA OAEP decrypt the recovery data.") + + // Perform recovery + require.NoError(f.SetRecover(recoveryKey)) + t.Log("Performed recovery, now checking status again...") + statusResponse, err := f.GetStatus() + require.NoError(err) + assert.EqualValues(3, gjson.Get(statusResponse, "data.StatusCode").Int(), "Server is in wrong status after recovery.") + + // Verify if old certificate is still valid + f.VerifyCertAfterRecovery(cert, cancelCoordinator, cfg) + }) + } } func TestRecoveryReset(t *testing.T) { diff --git a/test/manifests.go b/test/manifests.go index e0dbcddb6..4c85299d4 100644 --- a/test/manifests.go +++ b/test/manifests.go @@ -173,9 +173,11 @@ const ManifestJSON = `{ ] } }, - "FeatureGates": [ - "SignQuoteEndpoint" - ] + "Config": { + "FeatureGates": [ + "SignQuoteEndpoint" + ] + } }` // ManifestJSONWithRecoveryKey is a test manifest with a dynamically generated RSA key.