mirror of
https://github.com/lightninglabs/aperture.git
synced 2026-01-31 07:04:26 +01:00
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.
This commit is contained in:
92
secrets.go
Normal file
92
secrets.go
Normal file
@@ -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
|
||||
}
|
||||
115
secrets_test.go
Normal file
115
secrets_test.go
Normal file
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user