Skip to content

Commit

Permalink
keymanager: Simplify ephemeral secrets
Browse files Browse the repository at this point in the history
Store only the last ephemeral secret in the key manager state.
  • Loading branch information
peternose committed Mar 28, 2023
1 parent 910bb19 commit 900f093
Show file tree
Hide file tree
Showing 22 changed files with 280 additions and 426 deletions.
2 changes: 1 addition & 1 deletion .buildkite/code.pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ steps:
- "build-rust-runtime-loader"
- "build-rust-runtimes"
branches: "!master !stable/*"
parallelism: 3
parallelism: 4
timeout_in_minutes: 20
command:
- .buildkite/scripts/download_e2e_test_artifacts.sh
Expand Down
12 changes: 0 additions & 12 deletions go/consensus/tendermint/apps/keymanager/keymanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,6 @@ import (
registry "github.com/oasisprotocol/oasis-core/go/registry/api"
)

// maxEphemeralSecretAge is the maximum age of an ephemeral secret in the number of epochs.
const maxEphemeralSecretAge = 20

// minProposalReplicationPercent is the minimum percentage of enclaves in the key manager committee
// that must replicate the proposal for the next master secret before it is accepted.
const minProposalReplicationPercent = 66
Expand Down Expand Up @@ -212,15 +209,6 @@ func (app *keymanagerApplication) onEpochChange(ctx *tmapi.Context, epoch beacon
}
toEmit = append(toEmit, newStatus)
}

// Clean ephemeral secrets.
// TODO: use max ephemeral secret age from the key manager policy
if epoch > maxEphemeralSecretAge {
expiryEpoch := epoch - maxEphemeralSecretAge
if err = state.CleanEphemeralSecrets(ctx, rt.ID, expiryEpoch); err != nil {
return fmt.Errorf("failed to clean ephemeral secrets: %w", err)
}
}
}

// Note: It may be a good idea to sweep statuses that don't have runtimes,
Expand Down
7 changes: 3 additions & 4 deletions go/consensus/tendermint/apps/keymanager/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package keymanager
import (
"context"

beacon "github.com/oasisprotocol/oasis-core/go/beacon/api"
"github.com/oasisprotocol/oasis-core/go/common"
abciAPI "github.com/oasisprotocol/oasis-core/go/consensus/tendermint/api"
keymanagerState "github.com/oasisprotocol/oasis-core/go/consensus/tendermint/apps/keymanager/state"
Expand All @@ -15,7 +14,7 @@ type Query interface {
Status(context.Context, common.Namespace) (*keymanager.Status, error)
Statuses(context.Context) ([]*keymanager.Status, error)
MasterSecret(context.Context, common.Namespace) (*keymanager.SignedEncryptedMasterSecret, error)
EphemeralSecret(context.Context, common.Namespace, beacon.EpochTime) (*keymanager.SignedEncryptedEphemeralSecret, error)
EphemeralSecret(context.Context, common.Namespace) (*keymanager.SignedEncryptedEphemeralSecret, error)
Genesis(context.Context) (*keymanager.Genesis, error)
}

Expand Down Expand Up @@ -49,8 +48,8 @@ func (kq *keymanagerQuerier) MasterSecret(ctx context.Context, id common.Namespa
return kq.state.MasterSecret(ctx, id)
}

func (kq *keymanagerQuerier) EphemeralSecret(ctx context.Context, id common.Namespace, epoch beacon.EpochTime) (*keymanager.SignedEncryptedEphemeralSecret, error) {
return kq.state.EphemeralSecret(ctx, id, epoch)
func (kq *keymanagerQuerier) EphemeralSecret(ctx context.Context, id common.Namespace) (*keymanager.SignedEncryptedEphemeralSecret, error) {
return kq.state.EphemeralSecret(ctx, id)
}

func (app *keymanagerApplication) QueryFactory() interface{} {
Expand Down
52 changes: 25 additions & 27 deletions go/consensus/tendermint/apps/keymanager/state/interop/interop.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,40 +124,38 @@ func InitializeTestKeyManagerState(ctx context.Context, mkvs mkvs.Tree) error {
RSK: nil,
},
} {
if err := state.SetStatus(ctx, status); err != nil {
if err = state.SetStatus(ctx, status); err != nil {
return fmt.Errorf("setting key manager status: %w", err)
}
}

// Add two ephemeral secrets.
// Add an ephemeral secret.
rek1 := x25519.PrivateKey(sha512.Sum512_256([]byte("first rek")))
rek2 := x25519.PrivateKey(sha512.Sum512_256([]byte("second rek")))

for epoch := 1; epoch <= 2; epoch++ {
secret := kmApi.EncryptedEphemeralSecret{
ID: keymanager1,
Epoch: beacon.EpochTime(epoch),
Secret: kmApi.EncryptedSecret{
Checksum: []byte{1, 2, 3, 4, 5},
PubKey: *rek1.Public(),
Ciphertexts: map[x25519.PublicKey][]byte{
*rek1.Public(): {1, 2, 3},
*rek2.Public(): {4, 5, 6},
},
epoch := 1
secret := kmApi.EncryptedEphemeralSecret{
ID: keymanager1,
Epoch: beacon.EpochTime(epoch),
Secret: kmApi.EncryptedSecret{
Checksum: []byte{1, 2, 3, 4, 5},
PubKey: *rek1.Public(),
Ciphertexts: map[x25519.PublicKey][]byte{
*rek1.Public(): {1, 2, 3},
*rek2.Public(): {4, 5, 6},
},
}
sig, err := signature.Sign(signers[0], kmApi.EncryptedEphemeralSecretSignatureContext, cbor.Marshal(secret))
if err != nil {
return fmt.Errorf("failed to sign ephemeral secret: %w", err)
}
sigSecret := kmApi.SignedEncryptedEphemeralSecret{
Secret: secret,
Signature: sig.Signature,
}
err = state.SetEphemeralSecret(ctx, &sigSecret)
if err != nil {
return fmt.Errorf("failed to set ephemeral secret: %w", err)
}
},
}
sig, err := signature.Sign(signers[0], kmApi.EncryptedEphemeralSecretSignatureContext, cbor.Marshal(secret))
if err != nil {
return fmt.Errorf("failed to sign ephemeral secret: %w", err)
}
sigSecret := kmApi.SignedEncryptedEphemeralSecret{
Secret: secret,
Signature: sig.Signature,
}
err = state.SetEphemeralSecret(ctx, &sigSecret)
if err != nil {
return fmt.Errorf("failed to set ephemeral secret: %w", err)
}

return nil
Expand Down
40 changes: 4 additions & 36 deletions go/consensus/tendermint/apps/keymanager/state/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,12 @@ import (
"context"
"fmt"

beacon "github.com/oasisprotocol/oasis-core/go/beacon/api"
"github.com/oasisprotocol/oasis-core/go/common"
"github.com/oasisprotocol/oasis-core/go/common/cbor"
"github.com/oasisprotocol/oasis-core/go/common/keyformat"
abciAPI "github.com/oasisprotocol/oasis-core/go/consensus/tendermint/api"
"github.com/oasisprotocol/oasis-core/go/keymanager/api"
"github.com/oasisprotocol/oasis-core/go/storage/mkvs"
"github.com/oasisprotocol/oasis-core/go/storage/mkvs/node"
)

var (
Expand All @@ -30,7 +28,7 @@ var (
// ephemeralSecretKeyFmt is the key manager ephemeral secret key format.
//
// Value is CBOR-serialized key manager signed encrypted ephemeral secret.
ephemeralSecretKeyFmt = keyformat.New(0x73, keyformat.H(&common.Namespace{}), uint64(0))
ephemeralSecretKeyFmt = keyformat.New(0x73, keyformat.H(&common.Namespace{}))
)

// ImmutableState is the immutable key manager state wrapper.
Expand Down Expand Up @@ -122,8 +120,8 @@ func (st *ImmutableState) MasterSecret(ctx context.Context, id common.Namespace)
return &secret, nil
}

func (st *ImmutableState) EphemeralSecret(ctx context.Context, id common.Namespace, epoch beacon.EpochTime) (*api.SignedEncryptedEphemeralSecret, error) {
data, err := st.is.Get(ctx, ephemeralSecretKeyFmt.Encode(&id, uint64(epoch)))
func (st *ImmutableState) EphemeralSecret(ctx context.Context, id common.Namespace) (*api.SignedEncryptedEphemeralSecret, error) {
data, err := st.is.Get(ctx, ephemeralSecretKeyFmt.Encode(&id))
if err != nil {
return nil, abciAPI.UnavailableStateError(err)
}
Expand Down Expand Up @@ -175,40 +173,10 @@ func (st *MutableState) SetMasterSecret(ctx context.Context, secret *api.SignedE
}

func (st *MutableState) SetEphemeralSecret(ctx context.Context, secret *api.SignedEncryptedEphemeralSecret) error {
err := st.ms.Insert(ctx, ephemeralSecretKeyFmt.Encode(&secret.Secret.ID, uint64(secret.Secret.Epoch)), cbor.Marshal(secret))
err := st.ms.Insert(ctx, ephemeralSecretKeyFmt.Encode(&secret.Secret.ID), cbor.Marshal(secret))
return abciAPI.UnavailableStateError(err)
}

// CleanEphemeralSecrets removes all ephemeral secrets before the given epoch.
func (st *MutableState) CleanEphemeralSecrets(ctx context.Context, id common.Namespace, epoch beacon.EpochTime) error {
it := st.is.NewIterator(ctx)
defer it.Close()

hID := keyformat.PreHashed(id.Hash())

var toDelete []node.Key
for it.Seek(ephemeralSecretKeyFmt.Encode(&id)); it.Valid(); it.Next() {
var esID keyformat.PreHashed
var esEpoch beacon.EpochTime
if !ephemeralSecretKeyFmt.Decode(it.Key(), &esID, (*uint64)(&esEpoch)) {
break
}
if hID != esID || esEpoch >= epoch {
break
}
toDelete = append(toDelete, it.Key())
}
if it.Err() != nil {
return abciAPI.UnavailableStateError(it.Err())
}
for _, key := range toDelete {
if err := st.ms.Remove(ctx, key); err != nil {
return abciAPI.UnavailableStateError(err)
}
}
return nil
}

// NewMutableState creates a new mutable key manager state wrapper.
func NewMutableState(tree mkvs.KeyValueTree) *MutableState {
return &MutableState{
Expand Down
78 changes: 7 additions & 71 deletions go/consensus/tendermint/apps/keymanager/state/state_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,11 @@ func TestEphemeralSecret(t *testing.T) {
common.NewTestNamespaceFromSeed([]byte("runtime 1"), common.NamespaceKeyManager),
common.NewTestNamespaceFromSeed([]byte("runtime 2"), common.NamespaceKeyManager),
}
secrets := make([]*api.SignedEncryptedEphemeralSecret, 0, 20)
secrets := make([]*api.SignedEncryptedEphemeralSecret, 0, 10)
for i := 0; i < cap(secrets); i++ {
secret := api.SignedEncryptedEphemeralSecret{
Secret: api.EncryptedEphemeralSecret{
ID: runtimes[(i/5)%2],
ID: runtimes[i%2],
Epoch: beacon.EpochTime(i),
},
}
Expand All @@ -87,75 +87,11 @@ func TestEphemeralSecret(t *testing.T) {
}

// Test querying secrets.
for i := range secrets {
secret, err := s.EphemeralSecret(ctx, secrets[i].Secret.ID, secrets[i].Secret.Epoch)
for i, runtime := range runtimes {
secret, err := s.EphemeralSecret(ctx, runtime)
require.NoError(err, "EphemeralSecret()")
require.Equal(secrets[i], secret, "ephemeral secret should match")
}
for i := range secrets {
_, err := s.EphemeralSecret(ctx, secrets[i].Secret.ID, secrets[i].Secret.Epoch+5)
require.EqualError(err, api.ErrNoSuchEphemeralSecret.Error(), "EphemeralSecret should error for non-existing secrets")
}

// Test partial/complete secret removal.
testCases := []struct {
runtime common.Namespace
epoch beacon.EpochTime
removed int
kept int
}{
// Remove all secrets for the first runtime.
{
runtimes[0],
100,
10,
10,
},
// Remove 6 secrets (epochs 0-4, 10) for the first runtime.
{
runtimes[0],
11,
6,
14,
},
// Remove all secrets for the second runtime.
{
runtimes[1],
100,
10,
10,
},
// Remove 8 secrets (epochs 5-9, 15-17) for the second runtime.
{
runtimes[1],
18,
8,
12,
},
}
for _, tc := range testCases {
for _, secret := range secrets {
err := s.SetEphemeralSecret(ctx, secret)
require.NoError(err, "SetEphemeralSecret()")
}

err := s.CleanEphemeralSecrets(ctx, tc.runtime, tc.epoch)
require.NoError(err, "CleanEphemeralSecrets()")

var removed, kept int
for i := range secrets {
secret, err := s.EphemeralSecret(ctx, secrets[i].Secret.ID, secrets[i].Secret.Epoch)
switch {
case secrets[i].Secret.ID == tc.runtime && secrets[i].Secret.Epoch < tc.epoch:
require.EqualError(err, api.ErrNoSuchEphemeralSecret.Error(), "EphemeralSecret should error for non-existing secrets")
removed++
default:
require.NoError(err, "EphemeralSecret()")
require.Equal(secrets[i], secret, "ephemeral secret should match")
kept++
}
}
require.Equal(tc.removed, removed, "the number of removed ephemeral secrets is incorrect")
require.Equal(tc.kept, kept, "the number of kept ephemeral secrets is incorrect")
require.Equal(secrets[8+i], secret, "last ephemeral secret should be kept")
}
_, err := s.EphemeralSecret(ctx, common.Namespace{1, 2, 3})
require.EqualError(err, api.ErrNoSuchEphemeralSecret.Error(), "EphemeralSecret should error for non-existing secrets")
}
14 changes: 6 additions & 8 deletions go/consensus/tendermint/apps/keymanager/transactions.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,16 +249,14 @@ func (app *keymanagerApplication) publishEphemeralSecret(
return fmt.Errorf("keymanager: ephemeral secret can be published only by the key manager committee")
}

// Reject if the secret has been published.
_, err = state.EphemeralSecret(ctx, secret.Secret.ID, secret.Secret.Epoch)
switch err {
case nil:
return fmt.Errorf("keymanager: ephemeral secret for epoch %d already published", secret.Secret.Epoch)
case api.ErrNoSuchEphemeralSecret:
// Secret hasn't been published.
default:
// Reject if the ephemeral secret has been published in this epoch.
lastSecret, err := state.EphemeralSecret(ctx, secret.Secret.ID)
if err != nil && err != api.ErrNoSuchEphemeralSecret {
return err

Check warning on line 255 in go/consensus/tendermint/apps/keymanager/transactions.go

View check run for this annotation

Codecov / codecov/patch

go/consensus/tendermint/apps/keymanager/transactions.go#L255

Added line #L255 was not covered by tests
}
if lastSecret != nil && secret.Secret.Epoch == lastSecret.Secret.Epoch {
return fmt.Errorf("keymanager: ephemeral secret can be proposed once per epoch")
}

// Verify the secret. Ephemeral secrets can be published for the next epoch only.
epoch, err := app.state.GetCurrentEpoch(ctx)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,6 @@ func TestPublishEphemeralSecret(t *testing.T) {
t.Run("ephemeral secret already published", func(t *testing.T) {
sigSecret := newSignedSecret()
err := app.publishEphemeralSecret(txCtx, kmState, sigSecret)
require.EqualError(t, err, "keymanager: ephemeral secret for epoch 1 already published")
require.EqualError(t, err, "keymanager: ephemeral secret can be proposed once per epoch")
})
}
4 changes: 2 additions & 2 deletions go/consensus/tendermint/keymanager/keymanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,13 @@ func (sc *serviceClient) GetMasterSecret(ctx context.Context, query *registry.Na
return q.MasterSecret(ctx, query.ID)
}

func (sc *serviceClient) GetEphemeralSecret(ctx context.Context, query *registry.NamespaceEpochQuery) (*api.SignedEncryptedEphemeralSecret, error) {
func (sc *serviceClient) GetEphemeralSecret(ctx context.Context, query *registry.NamespaceQuery) (*api.SignedEncryptedEphemeralSecret, error) {
q, err := sc.querier.QueryAt(ctx, query.Height)
if err != nil {
return nil, err

Check warning on line 88 in go/consensus/tendermint/keymanager/keymanager.go

View check run for this annotation

Codecov / codecov/patch

go/consensus/tendermint/keymanager/keymanager.go#L88

Added line #L88 was not covered by tests
}

return q.EphemeralSecret(ctx, query.ID, query.Epoch)
return q.EphemeralSecret(ctx, query.ID)
}

func (sc *serviceClient) WatchMasterSecrets() (<-chan *api.SignedEncryptedMasterSecret, *pubsub.Subscription) {
Expand Down
4 changes: 2 additions & 2 deletions go/keymanager/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ var (
ErrNoSuchMasterSecret = errors.New(ModuleName, 3, "keymanager: no such master secret")

// ErrNoSuchEphemeralSecret is the error returned when a key manager ephemeral secret
// for the given epoch does not exist.
// does not exist.
ErrNoSuchEphemeralSecret = errors.New(ModuleName, 4, "keymanager: no such ephemeral secret")

// MethodUpdatePolicy is the method name for policy updates.
Expand Down Expand Up @@ -213,7 +213,7 @@ type Backend interface {
WatchMasterSecrets() (<-chan *SignedEncryptedMasterSecret, *pubsub.Subscription)

// GetEphemeralSecret returns the key manager ephemeral secret.
GetEphemeralSecret(context.Context, *registry.NamespaceEpochQuery) (*SignedEncryptedEphemeralSecret, error)
GetEphemeralSecret(context.Context, *registry.NamespaceQuery) (*SignedEncryptedEphemeralSecret, error)

// WatchEphemeralSecrets returns a channel that produces a stream of ephemeral secrets.
WatchEphemeralSecrets() (<-chan *SignedEncryptedEphemeralSecret, *pubsub.Subscription)
Expand Down

0 comments on commit 900f093

Please sign in to comment.