From 02f2a287b0baadb7361065c08e772347769e64b0 Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Wed, 20 Nov 2019 17:01:46 -0800 Subject: [PATCH] kirin: add etcd-backed secret store This will store the secret of each LSAT minted by the proxy, which is crucial for LSAT verification. The secrets are stored under a new "secrets" key prefixed by the top level LSAT etcd key, and each secret can be found by its unique identifier prefixed with the secrets key. --- secrets.go | 92 ++++++++++++++++++++++++++++++++++++++ secrets_test.go | 115 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 207 insertions(+) create mode 100644 secrets.go create mode 100644 secrets_test.go diff --git a/secrets.go b/secrets.go new file mode 100644 index 0000000..194d84d --- /dev/null +++ b/secrets.go @@ -0,0 +1,92 @@ +package kirin + +import ( + "context" + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "fmt" + "strings" + + "github.com/coreos/etcd/clientv3" + "github.com/lightninglabs/kirin/mint" + "github.com/lightninglabs/loop/lsat" +) + +var ( + // secretsPrefix is the key we'll use to prefix all LSAT identifiers + // with when storing secrets in an etcd cluster. + secretsPrefix = "secrets" +) + +// idKey returns the full key to store in the database for an LSAT identifier. +// The identifier is hex-encoded in order to prevent conflicts with the etcd key +// delimeter. +// +// The resulting path of the identifier bff4ee83 within etcd would look like: +// lsat/proxy/secrets/bff4ee83 +func idKey(id [sha256.Size]byte) string { + return strings.Join( + []string{topLevelKey, secretsPrefix, hex.EncodeToString(id[:])}, + etcdKeyDelimeter, + ) +} + +// secretStore is a store of LSAT secrets backed by an etcd cluster. +type secretStore struct { + *clientv3.Client +} + +// A compile-time constraint to ensure secretStore implements mint.SecretStore. +var _ mint.SecretStore = (*secretStore)(nil) + +// newSecretStore instantiates a new LSAT secrets store backed by an etcd +// cluster. +func newSecretStore(client *clientv3.Client) *secretStore { + return &secretStore{Client: client} +} + +// NewSecret creates a new cryptographically random secret which is keyed by the +// given hash. +func (s *secretStore) NewSecret(ctx context.Context, + id [sha256.Size]byte) ([lsat.SecretSize]byte, error) { + + var secret [lsat.SecretSize]byte + if _, err := rand.Read(secret[:]); err != nil { + return secret, err + } + + _, err := s.Put(ctx, idKey(id), string(secret[:])) + return secret, err +} + +// GetSecret returns the cryptographically random secret that corresponds to the +// given hash. If there is no secret, then mint.ErrSecretNotFound is returned. +func (s *secretStore) GetSecret(ctx context.Context, + id [sha256.Size]byte) ([lsat.SecretSize]byte, error) { + + resp, err := s.Get(ctx, idKey(id)) + if err != nil { + return [lsat.SecretSize]byte{}, err + } + if len(resp.Kvs) == 0 { + return [lsat.SecretSize]byte{}, mint.ErrSecretNotFound + } + if len(resp.Kvs[0].Value) != lsat.SecretSize { + return [lsat.SecretSize]byte{}, fmt.Errorf("invalid secret "+ + "size %v", len(resp.Kvs[0].Value)) + } + + var secret [lsat.SecretSize]byte + copy(secret[:], resp.Kvs[0].Value) + return secret, nil +} + +// RevokeSecret removes the cryptographically random secret that corresponds to +// the given hash. This acts as a NOP if the secret does not exist. +func (s *secretStore) RevokeSecret(ctx context.Context, + id [sha256.Size]byte) error { + + _, err := s.Delete(ctx, idKey(id)) + return err +} diff --git a/secrets_test.go b/secrets_test.go new file mode 100644 index 0000000..af93bb6 --- /dev/null +++ b/secrets_test.go @@ -0,0 +1,115 @@ +package kirin + +import ( + "bytes" + "context" + "crypto/sha256" + "io/ioutil" + "net/url" + "os" + "testing" + "time" + + "github.com/coreos/etcd/clientv3" + "github.com/coreos/etcd/embed" + "github.com/lightninglabs/kirin/mint" + "github.com/lightninglabs/loop/lsat" +) + +// etcdSetup is a helper that instantiates a new etcd cluster along with a +// client connection to it. A cleanup closure is also returned to free any +// allocated resources required by etcd. +func etcdSetup(t *testing.T) (*clientv3.Client, func()) { + t.Helper() + + tempDir, err := ioutil.TempDir("", "etcd") + if err != nil { + t.Fatalf("unable to create temp dir: %v", err) + } + + cfg := embed.NewConfig() + cfg.Dir = tempDir + cfg.LCUrls = []url.URL{{Host: "127.0.0.1:9125"}} + cfg.LPUrls = []url.URL{{Host: "127.0.0.1:9126"}} + + etcd, err := embed.StartEtcd(cfg) + if err != nil { + os.RemoveAll(tempDir) + t.Fatalf("unable to start etcd: %v", err) + } + + select { + case <-etcd.Server.ReadyNotify(): + case <-time.After(5 * time.Second): + os.RemoveAll(tempDir) + etcd.Server.Stop() // trigger a shutdown + t.Fatal("server took too long to start") + } + + client, err := clientv3.New(clientv3.Config{ + Endpoints: []string{cfg.LCUrls[0].Host}, + DialTimeout: 5 * time.Second, + }) + if err != nil { + t.Fatalf("unable to connect to etcd: %v", err) + } + + return client, func() { + etcd.Close() + os.RemoveAll(tempDir) + } +} + +// assertSecretExists is a helper to determine if a secret for the given +// identifier exists in the store. If it exists, its value is compared against +// the expected secret. +func assertSecretExists(t *testing.T, store *secretStore, id [sha256.Size]byte, + expSecret *[lsat.SecretSize]byte) { + + t.Helper() + + exists := expSecret != nil + secret, err := store.GetSecret(context.Background(), id) + switch { + case exists && err != nil: + t.Fatalf("unable to retrieve secret: %v", err) + case !exists && err != mint.ErrSecretNotFound: + t.Fatalf("expected error ErrSecretNotFound, got \"%v\"", err) + case exists: + if secret != *expSecret { + t.Fatalf("expected secret %x, got %x", expSecret, secret) + } + default: + return + } +} + +// TestSecretStore ensures the different operations of the secretStore behave as +// expected. +func TestSecretStore(t *testing.T) { + etcdClient, serverCleanup := etcdSetup(t) + defer etcdClient.Close() + defer serverCleanup() + + ctx := context.Background() + store := newSecretStore(etcdClient) + + // Create a test ID and ensure a secret doesn't exist for it yet as we + // haven't created one. + var id [sha256.Size]byte + copy(id[:], bytes.Repeat([]byte("A"), 32)) + assertSecretExists(t, store, id, nil) + + // Create one and ensure we can retrieve it at a later point. + secret, err := store.NewSecret(ctx, id) + if err != nil { + t.Fatalf("unable to generate new secret: %v", err) + } + assertSecretExists(t, store, id, &secret) + + // Once revoked, it should no longer exist. + if err := store.RevokeSecret(ctx, id); err != nil { + t.Fatalf("unable to revoke secret: %v", err) + } + assertSecretExists(t, store, id, nil) +}