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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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.

-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*.

+
+### 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.