Files
kms/custody_test.go
T
zeekayandHanzo Dev 5f92bdfb56
Docker Build & Push (Reusable) / create-manifest (push) Blocked by required conditions
Docker Build & Push (Reusable) / tag-single-arch (push) Blocked by required conditions
Docker Build & Push (Reusable) / deploy (push) Blocked by required conditions
Docker Build & Push (Reusable) / sbom (push) Blocked by required conditions
Docker Build & Push (Reusable) / build-amd64 (push) Waiting to run
Docker Build & Push (Reusable) / build-arm64 (push) Skipped
Build kms / build (push) Waiting to run
Release / Build binaries (linux/amd64) (push) Waiting to run
Release / GitHub Release (push) Blocked by required conditions
Release / Multi-arch Docker (ghcr.io/hanzoai/kms) (push) Blocked by required conditions
Release / Notify universe (auto-bump pin) (push) Blocked by required conditions
Release / Build binaries (darwin/amd64) (push) Failing after 0s
Release / Build binaries (darwin/arm64) (push) Failing after 0s
Release / Build binaries (linux/arm64) (push) Failing after 0s
feat(custody): hand the serving side a store, not the database
The custody store had no way out of a running KMS: Embedded.db is
unexported and there was no accessor, so cloud could not construct one at
all. (*Embedded).Custody() closes that.

It hands out the STORE rather than the database it lives in and never the
master key: a caller that needs to enrol a node has no business reading the
secret keyspace, and the key stays inside this module. It refuses when
KMS_MASTER_KEY_B64 is absent or malformed, because nothing can be sealed
without it and a custody surface that cannot seal must refuse rather than
come up holding node wallets under something weaker.

One store per Embedded, memoized on the struct rather than in a package
variable. The store's lock is what serializes enrolment and rotation, so a
second store over the same database would let two rotations interleave and
leave a subject with two active identities.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-05 23:22:18 -07:00

81 lines
2.4 KiB
Go

package kms
import (
"encoding/base64"
"errors"
"path/filepath"
"testing"
badger "github.com/luxfi/zapdb"
)
// embedded builds the minimum Embedded the custody accessor needs, without
// booting a listener: Custody reads the database and the record master key and
// nothing else.
func embedded(t *testing.T) *Embedded {
t.Helper()
db, err := badger.Open(badger.DefaultOptions(filepath.Join(t.TempDir(), "kms")).WithLogger(nil))
if err != nil {
t.Fatalf("open zapdb: %v", err)
}
t.Cleanup(func() { db.Close() })
return &Embedded{db: db}
}
// The serving side gets the store, never the database it lives in and never the
// master key. One store per Embedded, because that store's lock is what
// serializes enrolment and rotation.
func TestCustodyHandsOutOneStore(t *testing.T) {
t.Setenv("KMS_MASTER_KEY_B64", base64.StdEncoding.EncodeToString(make([]byte, 32)))
e := embedded(t)
first, err := e.Custody()
if err != nil {
t.Fatalf("Custody: %v", err)
}
second, err := e.Custody()
if err != nil {
t.Fatalf("second Custody: %v", err)
}
if first != second {
t.Fatal("Custody built a second store; two locks over one database can interleave a rotation")
}
}
// No key, no custody. A store that cannot seal must refuse rather than come up
// and hold node wallets under something weaker.
func TestCustodyRefusesWithoutTheRecordMasterKey(t *testing.T) {
for name, key := range map[string]struct {
value string
want error
}{
"absent": {"", errNoMasterKey},
"not base64": {"not-base64-at-all!!", errBadMasterKey},
"too short": {base64.StdEncoding.EncodeToString(make([]byte, 16)), errBadMasterKey},
"too long": {base64.StdEncoding.EncodeToString(make([]byte, 64)), errBadMasterKey},
} {
t.Run(name, func(t *testing.T) {
t.Setenv("KMS_MASTER_KEY_B64", key.value)
e := embedded(t)
store, err := e.Custody()
if !errors.Is(err, key.want) {
t.Fatalf("Custody with a %s key: got %v, want %v", name, err, key.want)
}
if store != nil {
t.Fatal("Custody returned a store it could not seal with")
}
})
}
}
func TestCustodyOnNothingIsRefused(t *testing.T) {
t.Setenv("KMS_MASTER_KEY_B64", base64.StdEncoding.EncodeToString(make([]byte, 32)))
var nilEmbedded *Embedded
if _, err := nilEmbedded.Custody(); err == nil {
t.Fatal("Custody on a nil Embedded returned a store")
}
if _, err := (&Embedded{}).Custody(); err == nil {
t.Fatal("Custody with no database returned a store")
}
}