Files
mesh-catalog/modules/mesh-vault/cmd/mesh-vault/vault_test.go
T

865 lines
33 KiB
Go

package main
// ADR 0349's vault test list, the part this package (V1, issue 674) answers: no bucket holds a secret; no
// private key is ever sent to the vault; every secret has a copy sealed to the operator key; `get` answers
// only an allowed asker, sealed to its key; `secret-changed` carries no value. The rest of that list (a
// consumer binding remakes the provider's credential, an accepted value is not remade, issued and typed
// values) is V2's and V5's.
import (
"crypto/ecdh"
"crypto/rand"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"os"
"path/filepath"
"regexp"
"strings"
"sync"
"testing"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/seal"
"golang.org/x/crypto/nacl/box"
)
// published is everything the vault said on the bus during a test, as it said it.
type published struct {
mu sync.Mutex
events []map[string]any
raw []string
}
func (p *published) emit(event string, body map[string]any) error {
p.mu.Lock()
defer p.mu.Unlock()
b, _ := json.Marshal(map[string]any{"event": event, "body": body})
p.raw = append(p.raw, string(b))
p.events = append(p.events, map[string]any{"event": event, "body": body})
return nil
}
// operatorKey is an operator key the way the controller's `operator key make` makes one: NaCl, raw X25519,
// base64 — the private half kept by the test, as the operator keeps it off the mesh.
func operatorKey(t *testing.T) (public, private string) {
t.Helper()
pub, priv, err := box.GenerateKey(rand.Reader)
if err != nil {
t.Fatal(err)
}
return base64.StdEncoding.EncodeToString(pub[:]), base64.StdEncoding.EncodeToString(priv[:])
}
// asPEM is a raw operator key as the SPKI PEM the vault takes.
func asPEM(t *testing.T, raw string) string {
t.Helper()
p, err := publicPEM(raw)
if err != nil {
t.Fatal(err)
}
return p
}
// openOperator opens an operator copy the way `secret recover --from-export` does.
func openOperator(t *testing.T, public, private, sealed string) string {
t.Helper()
var pub, priv [32]byte
p, _ := base64.StdEncoding.DecodeString(public)
q, _ := base64.StdEncoding.DecodeString(private)
copy(pub[:], p)
copy(priv[:], q)
blob, err := base64.StdEncoding.DecodeString(sealed)
if err != nil {
t.Fatal(err)
}
out, ok := box.OpenAnonymous(nil, blob, &pub, &priv)
if !ok {
t.Fatal("the operator copy does not open with the operator's key")
}
return string(out)
}
type testVault struct {
*Vault
dir string
bus *published
operatorPublic string
operatorPrivate string
}
func newVault(t *testing.T, askers Askers) testVault {
t.Helper()
dir := t.TempDir()
store, err := OpenStore(filepath.Join(dir, "store"))
if err != nil {
t.Fatal(err)
}
own, err := OwnKey(filepath.Join(dir, "state"), store)
if err != nil {
t.Fatal(err)
}
bus := &published{}
clock := time.Date(2026, 10, 14, 9, 0, 0, 0, time.UTC)
v := &Vault{Store: store, Own: own, Askers: askers, Emit: bus.emit,
Now: func() time.Time { clock = clock.Add(time.Second); return clock }, Log: t.Logf}
pub, priv := operatorKey(t)
if _, err := v.SetOperatorKey(asPEM(t, pub), "the test"); err != nil {
t.Fatal(err)
}
return testVault{Vault: v, dir: dir, bus: bus, operatorPublic: pub, operatorPrivate: priv}
}
// filesUnder is every file's content under dir.
func filesUnder(t *testing.T, dir string) map[string]string {
t.Helper()
out := map[string]string{}
_ = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err == nil && !info.IsDir() {
raw, _ := os.ReadFile(path)
out[path] = string(raw)
}
return nil
})
return out
}
// ---- no bucket holds a secret ---------------------------------------------------------------------------
// The manifest declares no bucket, and the code opens none: the vault's only place for a secret is its own
// store on its own disk (ADR 0349 §7, ADR 0201 §13). It reads one bucket of another's, and only reads it: the
// controller's assignments, for what each node's plan names (askers.go, design 57 V4).
func TestNoBucketHoldsASecret(t *testing.T) {
raw, err := os.ReadFile("../../module.json")
if err != nil {
t.Fatal(err)
}
var manifest map[string]any
if err := json.Unmarshal(raw, &manifest); err != nil {
t.Fatal(err)
}
if s, ok := manifest["state"]; ok {
t.Errorf("the manifest declares buckets %v: a secret is in no bucket, sealed or not (ADR 0201 §13)", s)
}
if r, _ := json.Marshal(manifest["reads"]); string(r) != `["mesh-controller.assignments"]` {
t.Errorf("the manifest reads %s: the vault reads the controller's assignments alone", r)
}
sources, _ := filepath.Glob("*.go")
bucket := regexp.MustCompile(`stdio\.State\(|nats-io|jetstream|KeyValue`)
for _, f := range sources {
if strings.HasSuffix(f, "_test.go") {
continue
}
code, _ := os.ReadFile(f)
// The reads it makes, and nothing that writes: Get and Keys alone, on the assignments (Keys for the
// binds it remakes a provider's credential on, pair.go).
code = []byte(strings.Replace(string(code), "stdio.State(AssignmentsRead).Get(", "", 1))
code = []byte(strings.Replace(string(code), "stdio.State(AssignmentsRead).Keys(", "", 1))
if m := bucket.Find(code); m != nil {
t.Errorf("%s opens a bucket (%s): the vault keeps every secret in its own store only", f, m)
}
}
// And what the vault publishes while it holds, changes and hands out secrets carries none of them, in
// the clear or sealed.
v := newVault(t, AllowList{"g14.node-provisioner": {"pair/postgres/gitea"}})
value := "the-password-nobody-may-see"
if _, err := v.Put("pair/postgres/gitea", value, "the test"); err != nil {
t.Fatal(err)
}
asker, _ := seal.GenerateKeyPair()
answer, err := v.Get("g14.node-provisioner", "pair/postgres/gitea", asker.PublicKey)
if err != nil {
t.Fatal(err)
}
rec, _, _ := v.Store.Read("pair/postgres/gitea")
for _, said := range v.bus.raw {
for _, secret := range []string{value, answer.Sealed.Ct, rec.AtRest.Ct, rec.Operator.Sealed} {
if strings.Contains(said, secret) {
t.Errorf("the vault published %s, which carries a secret or a sealed copy of one", said)
}
}
}
}
// Every secret is encrypted at rest: no file the vault writes holds a value in the clear, and each opens
// only with the vault's own key.
func TestEverySecretIsEncryptedAtRest(t *testing.T) {
v := newVault(t, nil)
values := map[string]string{"own/g14/dunst/token": "tok-1-plain", "pair/postgres/gitea": "pw-2-plain"}
for name, value := range values {
if _, err := v.Put(name, value, "the test"); err != nil {
t.Fatal(err)
}
}
for path, content := range filesUnder(t, filepath.Join(v.dir, "store")) {
for _, value := range values {
if strings.Contains(content, value) {
t.Errorf("%s holds a value in the clear", path)
}
}
if info, _ := os.Stat(path); info.Mode().Perm()&0o077 != 0 {
t.Errorf("%s is readable by others: %v", path, info.Mode())
}
}
for name, value := range values {
rec, ok, err := v.Store.Read(name)
if err != nil || !ok {
t.Fatalf("%s: %v %v", name, ok, err)
}
got, err := seal.Open(rec.AtRest, v.Own.PrivateKey)
if err != nil || got != value {
t.Errorf("%s at rest opens to %q (%v), not its value", name, got, err)
}
}
}
// ---- no private key is accepted -------------------------------------------------------------------------
func TestNoPrivateKeyIsAccepted(t *testing.T) {
v := newVault(t, AllowList{"g14.node-provisioner": {"pair/postgres/gitea"}})
keys, _ := seal.GenerateKeyPair()
ssh := "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAA\n-----END OPENSSH PRIVATE KEY-----\n"
rsa := "-----BEGIN RSA PRIVATE KEY-----\nMIIBOgIBAAJBAK\n-----END RSA PRIVATE KEY-----\n"
// Not as a secret to hold: a private key stays where it was made (ADR 0113 §2).
for _, value := range []string{keys.PrivateKey, ssh, rsa, "prefix " + keys.PrivateKey} {
if _, err := v.Put("own/g14/sshd/host", value, "the test"); err == nil {
t.Errorf("the vault took a private key as a secret: %.40q", value)
}
}
if _, ok, _ := v.Store.Read("own/g14/sshd/host"); ok {
t.Error("a refused private key was kept all the same")
}
// Not as the key to seal an answer to.
if _, err := v.Put("pair/postgres/gitea", "pw", "the test"); err != nil {
t.Fatal(err)
}
raw32 := base64.StdEncoding.EncodeToString(make([]byte, 32))
for _, key := range []string{keys.PrivateKey, ssh, raw32, "", "not a key"} {
if _, err := v.Get("g14.node-provisioner", "pair/postgres/gitea", key); err == nil {
t.Errorf("get sealed an answer to %.40q, which is not a PEM public key", key)
}
}
// Not as the operator key: neither a PEM private key nor a raw key, which looks the same as a raw
// private one.
rawPub, rawPriv := operatorKey(t)
for _, key := range []string{keys.PrivateKey, rawPriv, rawPub} {
if _, err := v.SetOperatorKey(key, "the test"); err == nil {
t.Errorf("the vault took %.20q as the operator key", key)
}
}
if k, _ := v.Store.OperatorKey(); k != v.operatorPublic {
t.Error("a refused operator key replaced the operator key")
}
// And nothing it publishes or writes names one that it refused.
for _, said := range v.bus.raw {
if strings.Contains(said, "PRIVATE KEY") {
t.Errorf("the vault published a private key: %s", said)
}
}
for path, content := range filesUnder(t, filepath.Join(v.dir, "store")) {
if strings.Contains(content, "PRIVATE KEY") {
t.Errorf("%s holds a private key", path)
}
}
}
// ---- every secret has an operator-sealed copy -----------------------------------------------------------
func TestEverySecretHasAnOperatorSealedCopy(t *testing.T) {
v := newVault(t, nil)
values := map[string]string{"own/g14/dunst/token": "a", "pair/postgres/gitea": "b"}
for name, value := range values {
if _, err := v.Put(name, value, "the test"); err != nil {
t.Fatal(err)
}
}
if _, err := v.Mint("pair/postgres/keycloak", "the test"); err != nil {
t.Fatal(err)
}
all, err := v.Store.List()
if err != nil {
t.Fatal(err)
}
if len(all) != 3 {
t.Fatalf("the store lists %d secrets, not 3", len(all))
}
for _, rec := range all {
if rec.Operator.Key != v.operatorPublic || rec.Operator.Box != OperatorBox {
t.Errorf("%s is sealed to %q in %q, not to the operator key", rec.Name, rec.Operator.Key, rec.Operator.Box)
}
got := openOperator(t, v.operatorPublic, v.operatorPrivate, rec.Operator.Sealed)
want, _ := seal.Open(rec.AtRest, v.Own.PrivateKey)
if got != want || got == "" {
t.Errorf("%s: the operator copy opens to %q, the vault's to %q", rec.Name, got, want)
}
}
// A new operator key: every copy is sealed again to it, and none stays sealed to the old one.
pub, priv := operatorKey(t)
n, err := v.SetOperatorKey(asPEM(t, pub), "the test")
if err != nil || n != 3 {
t.Fatalf("a new operator key sealed %d copies again (%v), not 3", n, err)
}
all, _ = v.Store.List()
for _, rec := range all {
if rec.Operator.Key != pub {
t.Errorf("%s is still sealed to the earlier operator key", rec.Name)
}
openOperator(t, pub, priv, rec.Operator.Sealed)
}
}
// Without an operator key the vault keeps nothing: a secret only it could open is what ADR 0113 §22 rules
// out, so the refusal is loud rather than a copy quietly missing.
func TestNoOperatorKeyKeepsNothing(t *testing.T) {
dir := t.TempDir()
store, _ := OpenStore(filepath.Join(dir, "store"))
own, _ := OwnKey(filepath.Join(dir, "state"), store)
v := &Vault{Store: store, Own: own, Emit: (&published{}).emit, Now: time.Now, Log: t.Logf}
if _, err := v.Put("own/g14/dunst/token", "x", "the test"); err == nil || !strings.Contains(err.Error(), "operator key") {
t.Fatalf("a secret was kept with no operator key to seal its copy to: %v", err)
}
if all, _ := store.List(); len(all) != 0 {
t.Fatalf("the store holds %d secrets", len(all))
}
}
// ---- get answers only an allowed asker, sealed to its key -----------------------------------------------
func TestGetAnswersOnlyAnAllowedAskerSealedToItsKey(t *testing.T) {
v := newVault(t, AllowList{"g14.node-provisioner": {"pair/postgres/gitea"}})
if _, err := v.Put("pair/postgres/gitea", "the-value", "the test"); err != nil {
t.Fatal(err)
}
if _, err := v.Put("own/ace/dunst/token", "another", "the test"); err != nil {
t.Fatal(err)
}
asker, _ := seal.GenerateKeyPair()
answer, err := v.Get("g14.node-provisioner", "pair/postgres/gitea", asker.PublicKey)
if err != nil {
t.Fatal(err)
}
if got, err := seal.Open(answer.Sealed, asker.PrivateKey); err != nil || got != "the-value" {
t.Fatalf("the answer opens to %q (%v)", got, err)
}
other, _ := seal.GenerateKeyPair()
if _, err := seal.Open(answer.Sealed, other.PrivateKey); err == nil {
t.Error("the answer opens with a key it was not sealed to")
}
if _, err := seal.Open(answer.Sealed, v.Own.PrivateKey); err == nil {
t.Error("the answer opens with the vault's own key")
}
if answer.Generation != 1 || answer.Name != "pair/postgres/gitea" {
t.Errorf("the answer says %+v", answer)
}
refusals := []struct{ caller, name, why string }{
{"ace.node-provisioner", "pair/postgres/gitea", "an asker not on the list"},
{"g14.node-provisioner", "own/ace/dunst/token", "a secret the asker is not allowed"},
{stdio.Unproven, "pair/postgres/gitea", "a call whose caller the runtime did not prove"},
{"", "pair/postgres/gitea", "no caller at all"},
{"g14.node-provisioner", "pair/postgres/nothing", "a secret the vault does not hold"},
}
for _, r := range refusals {
if a, err := v.Get(r.caller, r.name, asker.PublicKey); err == nil {
t.Errorf("get answered %s: %+v", r.why, a)
}
}
}
// A vault given no rule answers nobody.
func TestAVaultWithNoRuleAnswersNobody(t *testing.T) {
v := newVault(t, nil)
if _, err := v.Put("pair/postgres/gitea", "x", "the test"); err != nil {
t.Fatal(err)
}
asker, _ := seal.GenerateKeyPair()
for _, caller := range []string{"provisioner.g14", "novox.mesh-provisioner", "controller", "person.jochen", "novox.node-tools"} {
if _, err := v.Get(caller, "pair/postgres/gitea", asker.PublicKey); err == nil {
t.Errorf("a vault with no rule answered %s", caller)
}
}
}
// assignments stands in for the controller's assignments mirror as the vault reads it.
type assignments map[string]string
func (a assignments) read(key string) ([]byte, error) {
if v, ok := a[key]; ok {
return []byte(v), nil
}
return nil, nil
}
// The running vault's rule (ADR 0349 §11, design 57 V4): a node's provisioner client, under its own account, for a
// secret of a module on its node that the module's plan there names, and nobody else for anything else. Each
// refusal is recorded with why.
func TestTheRunningVaultAnswersAMachinesClientForWhatItsPlanNames(t *testing.T) {
mirror := assignments{
"g14.dunst": `{"node":"g14","module":"dunst","assigned":"2026-10-14T20:00:00Z","provisioning":` +
`{"templates":{"source":"x","digest":"y"},"files":[],"secrets":[{"name":"token"}]}}`,
"g14.picom": `{"node":"g14","module":"picom","assigned":"2026-10-14T20:00:00Z"}`,
"ace.dunst": `{"node":"ace","module":"dunst","assigned":"2026-10-14T20:00:00Z","provisioning":{"secrets":[{"name":"token"}]}}`,
"g14.liar": `{"node":"ace","module":"liar","provisioning":{"secrets":[{"name":"token"}]}}`,
"g14.unparsed": `not json`,
}
v := newVault(t, Declared{Read: mirror.read})
for _, name := range []string{"own/g14/dunst/token", "own/g14/dunst/other", "own/g14/picom/token",
"own/ace/dunst/token", "own/g14/liar/token", "own/g14/unparsed/token", "provision/novox/postgres",
"own/g14/absent/token"} {
if _, err := v.Put(name, "value-of-"+name, "the test"); err != nil {
t.Fatal(err)
}
}
asker, _ := seal.GenerateKeyPair()
answer, err := v.Get("provisioner.g14", "own/g14/dunst/token", asker.PublicKey)
if err != nil {
t.Fatalf("the machine's client was refused the secret its module's plan names: %v", err)
}
if got, err := seal.Open(answer.Sealed, asker.PrivateKey); err != nil || got != "value-of-own/g14/dunst/token" {
t.Fatalf("the answer opens to %q (%v)", got, err)
}
refusals := []struct{ caller, name, why string }{
{"provisioner.g14", "own/g14/dunst/other", "a secret the plan does not name"},
{"provisioner.g14", "own/g14/picom/token", "a module not handed to the provisioner"},
{"provisioner.g14", "own/ace/dunst/token", "another machine's secret"},
{"provisioner.ace", "own/g14/dunst/token", "another machine's client"},
{"provisioner.g14", "own/g14/absent/token", "a module not assigned there"},
{"provisioner.g14", "own/g14/liar/token", "an assignment whose value names another machine"},
{"provisioner.g14", "own/g14/unparsed/token", "an assignment that is not the controller's"},
{"provisioner.g14", "provision/novox/postgres", "a provision's credential the vault made for nobody there"},
{"g14.node-tools", "own/g14/dunst/token", "the tool runner's account, which an agent shares"},
{"g14.dunst", "own/g14/dunst/token", "a module's own account, until issue 569"},
{"controller", "own/g14/dunst/token", "the controller"},
{"provisioner.", "own/g14/dunst/token", "a client naming no machine"},
}
for _, r := range refusals {
if a, err := v.Get(r.caller, r.name, asker.PublicKey); err == nil {
t.Errorf("get answered %s (%s asking %s): %+v", r.why, r.caller, r.name, a)
}
}
handed, err := v.Store.Handed(100)
if err != nil {
t.Fatal(err)
}
refused := 0
for _, h := range handed {
if h.Outcome == "refused" {
if h.Why == "" {
t.Errorf("a refusal of %s to %s is recorded without why", h.Name, h.To)
}
refused++
}
}
// Every refusal but the last, whose caller is not a bus user's shape and so fills no record (vault.go).
if refused != len(refusals)-1 {
t.Errorf("%d refusals recorded, want %d", refused, len(refusals)-1)
}
}
// A value a provider's software issued for one holder (issue 757): the running rule hands
// provision/<provider>/<provision>/<node>/<module> to that node's client when the module's plan there names the
// provision from that provider, and refuses another node's, another module's, another provider's and the provision's
// one credential.
func TestTheRunningVaultAnswersAMachinesClientForAnIssuedValueItsPlanNames(t *testing.T) {
plan := `{"templates":{"source":"x","digest":"y"},"files":[],` +
`"provisions":[{"name":"gitea-read","provision":"gitea-read","provider":"novox","path":"/s/gitea-read.secret"}]}`
mirror := assignments{
"anchor.claude-code": `{"node":"anchor","module":"claude-code","provisioning":` + plan + `}`,
"laptop.claude-code": `{"node":"laptop","module":"claude-code","provisioning":` + plan + `}`,
"anchor.zsh": `{"node":"anchor","module":"zsh","provisioning":{"secrets":[{"name":"token"}]}}`,
"anchor.picom": `{"node":"anchor","module":"picom"}`,
}
v := newVault(t, Declared{Read: mirror.read})
for _, name := range []string{anchorsToken, laptopsToken, "provision/novox/gitea-read/anchor/zsh",
"provision/novox/gitea-read/anchor/picom", "provision/ace/gitea-read/anchor/claude-code",
"provision/novox/other/anchor/claude-code", "provision/novox/gitea-read",
"provision/novox/gitea-read/anchor/absent"} {
if _, err := v.Put(name, "value-of-"+name, "the test"); err != nil {
t.Fatal(err)
}
}
asker, _ := seal.GenerateKeyPair()
answer, err := v.Get("provisioner.anchor", anchorsToken, asker.PublicKey)
if err != nil {
t.Fatalf("the machine's client was refused the value issued for its module, which its plan names: %v", err)
}
if got, err := seal.Open(answer.Sealed, asker.PrivateKey); err != nil || got != "value-of-"+anchorsToken {
t.Fatalf("the answer opens to %q (%v)", got, err)
}
refusals := []struct{ caller, name, why string }{
{"provisioner.anchor", laptopsToken, "another machine's holder value"},
{"provisioner.laptop", anchorsToken, "another machine's holder value, the other way"},
{"provisioner.anchor", "provision/novox/gitea-read/anchor/zsh", "another module's, whose plan names it not"},
{"provisioner.anchor", "provision/novox/gitea-read/anchor/picom", "a module not handed to the provisioner"},
{"provisioner.anchor", "provision/novox/gitea-read/anchor/absent", "a module not assigned there"},
{"provisioner.anchor", "provision/ace/gitea-read/anchor/claude-code", "another provider's"},
{"provisioner.anchor", "provision/novox/other/anchor/claude-code", "a provision the plan names not"},
{"provisioner.anchor", "provision/novox/gitea-read", "the provision's one credential"},
{"anchor.node-tools", anchorsToken, "the tool runner's account, which an agent shares"},
{"anchor.claude-code", anchorsToken, "the module's own account, until issue 569"},
}
for _, r := range refusals {
if a, err := v.Get(r.caller, r.name, asker.PublicKey); err == nil {
t.Errorf("get answered %s (%s asking %s): %+v", r.why, r.caller, r.name, a)
}
}
}
// The node-engine's sealing key (mesh-host internal/identity/sealing.go: raw X25519, base64, the NaCl
// anonymous box) is the same curve as the ADR 0206 box. A node's client asks with that key's public half as
// SPKI PEM, and opens the answer with its private half as PKCS #8 — no second key on the node, and no NaCl
// in the vault's answers. This is the path V4 builds on.
func TestANodesSealingKeyOpensTheAnswer(t *testing.T) {
v := newVault(t, AllowList{"g14.node-provisioner": {"pair/postgres/gitea"}})
if _, err := v.Put("pair/postgres/gitea", "for-the-node", "the test"); err != nil {
t.Fatal(err)
}
// As mesh-host's GenerateSealingKey makes it.
priv, _ := ecdh.X25519().GenerateKey(rand.Reader)
nodePublic := base64.StdEncoding.EncodeToString(priv.PublicKey().Bytes())
nodePrivate := base64.StdEncoding.EncodeToString(priv.Bytes())
rawPub, _ := base64.StdEncoding.DecodeString(nodePublic)
pubKey, _ := ecdh.X25519().NewPublicKey(rawPub)
spki, _ := x509.MarshalPKIXPublicKey(pubKey)
askWith := string(pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: spki}))
answer, err := v.Get("g14.node-provisioner", "pair/postgres/gitea", askWith)
if err != nil {
t.Fatal(err)
}
rawPriv, _ := base64.StdEncoding.DecodeString(nodePrivate)
privKey, _ := ecdh.X25519().NewPrivateKey(rawPriv)
pkcs8, _ := x509.MarshalPKCS8PrivateKey(privKey)
openWith := string(pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: pkcs8}))
if got, err := seal.Open(answer.Sealed, openWith); err != nil || got != "for-the-node" {
t.Fatalf("the node's own sealing key does not open the answer: %q %v", got, err)
}
}
// ---- secret-changed carries no value --------------------------------------------------------------------
func TestSecretChangedCarriesNoValue(t *testing.T) {
v := newVault(t, nil)
if _, err := v.Put("pair/postgres/gitea", "first-value", "g14.mesh-vault-test"); err != nil {
t.Fatal(err)
}
if _, err := v.Put("pair/postgres/gitea", "second-value", "person.jochen"); err != nil {
t.Fatal(err)
}
if _, err := v.Mint("own/g14/dunst/token", "the vault"); err != nil {
t.Fatal(err)
}
var changed []map[string]any
for _, e := range v.bus.events {
if e["event"] == ChangedEvent {
changed = append(changed, e["body"].(map[string]any))
}
}
if len(changed) != 3 {
t.Fatalf("%d secret-changed events, not 3: %v", len(changed), v.bus.events)
}
for i, body := range changed {
keys := make([]string, 0, len(body))
for k := range body {
keys = append(keys, k)
}
for _, k := range keys {
switch k {
case "name", "by", "at", "generation":
default:
t.Errorf("secret-changed carries %q, beyond who, when, which and its generation", k)
}
}
if body["by"] == "" || body["at"] == "" {
t.Errorf("secret-changed %d says not who or when: %v", i, body)
}
}
if changed[1]["by"] != "person.jochen" || changed[1]["generation"] != 2 {
t.Errorf("the second change says %v", changed[1])
}
minted, _, _ := v.Store.Read("own/g14/dunst/token")
mintedValue, _ := seal.Open(minted.AtRest, v.Own.PrivateKey)
for _, said := range v.bus.raw {
for _, value := range []string{"first-value", "second-value", mintedValue} {
if strings.Contains(said, value) {
t.Errorf("an event carries a value: %s", said)
}
}
}
}
// The same value again is no change: no new generation, no event.
func TestTheSameValueAgainIsNoChange(t *testing.T) {
v := newVault(t, nil)
for i := 0; i < 2; i++ {
if _, err := v.Put("pair/postgres/gitea", "same", "the test"); err != nil {
t.Fatal(err)
}
}
rec, _, _ := v.Store.Read("pair/postgres/gitea")
if rec.Generation != 1 {
t.Errorf("the same value twice is generation %d", rec.Generation)
}
n := 0
for _, e := range v.bus.events {
if e["event"] == ChangedEvent {
n++
}
}
if n != 1 {
t.Errorf("%d secret-changed events for one value", n)
}
}
// A secret's name is a name, not a path.
func TestASecretsNameIsNotAPath(t *testing.T) {
v := newVault(t, nil)
for _, name := range []string{"../etc/passwd", "/abs", "a//b", "A/b", "a/../b", "", strings.Repeat("a", 300)} {
if _, err := v.Put(name, "x", "the test"); err == nil {
t.Errorf("the vault kept a secret named %q", name)
}
}
}
// The vault's own key is made once, kept beside it, and the same on the next start.
func TestTheVaultsOwnKeyIsMadeOnce(t *testing.T) {
dir := t.TempDir()
a, err := OwnKey(dir, nil)
if err != nil {
t.Fatal(err)
}
b, err := OwnKey(dir, nil)
if err != nil {
t.Fatal(err)
}
if a != b {
t.Error("the vault's key changed between two starts")
}
info, _ := os.Stat(filepath.Join(dir, OwnKeyFile))
if info.Mode().Perm() != 0o600 {
t.Errorf("the vault's key is %v", info.Mode())
}
}
// The vault's key gone while its store holds secrets: no new key is made, and the vault does not start.
func TestAMissingKeyBesideSecretsIsRefused(t *testing.T) {
v := newVault(t, nil)
if _, err := v.Put("pair/postgres/gitea", "x", "the test"); err != nil {
t.Fatal(err)
}
if err := os.Remove(filepath.Join(v.dir, "state", OwnKeyFile)); err != nil {
t.Fatal(err)
}
if _, err := OwnKey(filepath.Join(v.dir, "state"), v.Store); err == nil || !strings.Contains(err.Error(), "MISSING") {
t.Fatalf("a new key was made beside secrets sealed to the lost one: %v", err)
}
if _, err := os.Stat(filepath.Join(v.dir, "state", OwnKeyFile)); err == nil {
t.Error("a key file was written all the same")
}
}
// A new operator key that fails partway leaves no copy sealed to the earlier key for good: the next call
// with the same key — every minute, from the mesh's export — finishes the rest.
func TestANewOperatorKeyFailingPartwayIsFinished(t *testing.T) {
v := newVault(t, nil)
for _, name := range []string{"a/one", "b/two", "c/three"} {
if _, err := v.Mint(name, "the test"); err != nil {
t.Fatal(err)
}
}
pub, priv := operatorKey(t)
v.Store.failWrite = func(r Record) error {
if r.Name == "b/two" {
return os.ErrPermission
}
return nil
}
n, err := v.SetOperatorKey(asPEM(t, pub), "the test")
if err == nil || n != 2 {
t.Fatalf("a failure partway said %d sealed again, err %v", n, err)
}
if left, _ := v.StillSealedToAnother(); left != 1 {
t.Fatalf("%d copies left on the earlier key, not 1", left)
}
v.Store.failWrite = nil
// The path the minute takes while the export holds the key: the same key again.
if n, err := v.SetOperatorKey(asPEM(t, pub), "the test"); err != nil || n != 1 {
t.Fatalf("the same key again sealed %d again (%v), not the 1 left", n, err)
}
if n, err := v.SetOperatorKey(asPEM(t, pub), "the test"); err != nil || n != 0 {
t.Fatalf("the same key a third time sealed %d (%v)", n, err)
}
all, _ := v.Store.List()
for _, rec := range all {
if rec.Operator.Key != pub {
t.Errorf("%s is still sealed to an earlier key", rec.Name)
}
openOperator(t, pub, priv, rec.Operator.Sealed)
}
}
// Who was handed which secret, and when, and who was refused, is in the vault's own store.
func TestWhoWasHandedWhatIsRecordedInTheStore(t *testing.T) {
v := newVault(t, AllowList{"g14.node-provisioner": {"pair/postgres/gitea"}})
if _, err := v.Put("pair/postgres/gitea", "the-value", "the test"); err != nil {
t.Fatal(err)
}
asker, _ := seal.GenerateKeyPair()
if _, err := v.Get("g14.node-provisioner", "pair/postgres/gitea", asker.PublicKey); err != nil {
t.Fatal(err)
}
_, _ = v.Get("ace.node-provisioner", "pair/postgres/gitea", asker.PublicKey)
handed, err := v.Store.Handed(10)
if err != nil || len(handed) != 2 {
t.Fatalf("%v %v", handed, err)
}
if handed[1].To != "g14.node-provisioner" || handed[1].Outcome != "handed" || handed[1].Generation != 1 || handed[1].At.IsZero() {
t.Errorf("the hand-out is recorded as %+v", handed[1])
}
if handed[0].To != "ace.node-provisioner" || handed[0].Outcome != "refused" || handed[0].Why == "" {
t.Errorf("the refusal is recorded as %+v", handed[0])
}
raw, _ := os.ReadFile(filepath.Join(v.dir, "store", "handed.jsonl"))
if strings.Contains(string(raw), "the-value") {
t.Error("the record of hand-outs holds a value")
}
}
// The vault's operator copies reach the operator's recovery: secret_vault_export is a document
// `secret recover --from-export` reads, found by node "vault", module "mesh-vault" and the secret's name,
// and opened with the operator's private key as that command opens it (mesh-controller
// cmd/mesh-controller/secret.go keptFromExport, internal/secrets Open).
func TestTheOperatorRecoversAVaultSecretFromItsExport(t *testing.T) {
v := newVault(t, nil)
if _, err := v.Put("pair/postgres/gitea", "recover-me", "the test"); err != nil {
t.Fatal(err)
}
answer, err := toolNamed(t, definitions(&Env{Vault: v.Vault}), "secret_vault_export").Run(map[string]any{})
if err != nil {
t.Fatal(err)
}
file := filepath.Join(t.TempDir(), "vault-export.json")
raw, _ := json.Marshal(answer)
_ = os.WriteFile(file, raw, 0o600)
// What `secret recover vault mesh-vault pair/postgres/gitea --from-export <file>` reads.
var e struct {
Kept []struct {
Node, Module, Name, Sealed, Key string
} `json:"kept"`
}
read, _ := os.ReadFile(file)
if err := json.Unmarshal(read, &e); err != nil {
t.Fatal(err)
}
var sealed string
for _, k := range e.Kept {
if k.Node == "vault" && k.Module == "mesh-vault" && k.Name == "pair/postgres/gitea" {
sealed = k.Sealed
if k.Key != v.operatorPublic {
t.Errorf("the export names key %s, not the operator's", k.Key)
}
}
}
if sealed == "" {
t.Fatalf("the export holds no copy of the secret: %s", read)
}
// secrets.Open: the public half derived from the private key, then the anonymous box.
privRaw, _ := base64.StdEncoding.DecodeString(v.operatorPrivate)
key, _ := ecdh.X25519().NewPrivateKey(privRaw)
if got := openOperator(t, base64.StdEncoding.EncodeToString(key.PublicKey().Bytes()), v.operatorPrivate, sealed); got != "recover-me" {
t.Fatalf("recovered %q", got)
}
if strings.Contains(string(read), "recover-me") {
t.Error("the export holds the value in the clear")
}
}
// With no key in the export the minute calls Reseal, which finishes what is pending too.
func TestResealFinishesWhatIsPending(t *testing.T) {
v := newVault(t, nil)
if _, err := v.Mint("a/one", "the test"); err != nil {
t.Fatal(err)
}
pub, _ := operatorKey(t)
v.Store.failWrite = func(Record) error { return os.ErrPermission }
if _, err := v.SetOperatorKey(asPEM(t, pub), "the test"); err == nil {
t.Fatal("a failing write was not said")
}
v.Store.failWrite = nil
if n, err := v.Reseal(); err != nil || n != 1 {
t.Fatalf("%d %v", n, err)
}
}
// A secret is not handed out when who was handed it cannot be recorded.
func TestNoHandOutWithoutItsRecord(t *testing.T) {
v := newVault(t, AllowList{"g14.node-provisioner": {"pair/postgres/gitea"}})
if _, err := v.Put("pair/postgres/gitea", "x", "the test"); err != nil {
t.Fatal(err)
}
if err := os.Mkdir(filepath.Join(v.dir, "store", "handed.jsonl"), 0o700); err != nil {
t.Fatal(err)
}
asker, _ := seal.GenerateKeyPair()
if a, err := v.Get("g14.node-provisioner", "pair/postgres/gitea", asker.PublicKey); err == nil {
t.Fatalf("handed out with no record of it: %+v", a)
}
}
// What an asker cannot shape fills no record: a name that is no secret's name, a caller that is no bus user.
func TestAnArbitraryAskFillsNoRecord(t *testing.T) {
v := newVault(t, nil)
asker, _ := seal.GenerateKeyPair()
for _, ask := range [][2]string{{"g14.x", strings.Repeat("junk ", 1000)}, {"g14.x", "../../etc"},
{"not a bus user!", "pair/a"}, {strings.Repeat("a", 300), "pair/a"}} {
_, _ = v.Get(ask[0], ask[1], asker.PublicKey)
}
if h, _ := v.Store.Handed(10); len(h) != 0 {
t.Errorf("recorded %d arbitrary asks: %+v", len(h), h)
}
}
// The record of hand-outs is bounded: rolled at its size into a fixed number of files, the oldest dropped.
func TestTheRecordOfHandOutsIsBounded(t *testing.T) {
v := newVault(t, nil) // refuses everyone, as the running vault does
v.Store.HandedMax = 2000
asker, _ := seal.GenerateKeyPair()
for i := 0; i < 500; i++ {
_, _ = v.Get("g14.flood", "pair/postgres/gitea", asker.PublicKey)
}
var total int64
files, _ := filepath.Glob(filepath.Join(v.dir, "store", "handed.jsonl*"))
if len(files) != HandedKept+1 {
t.Errorf("%d files of the record, not %d: %v", len(files), HandedKept+1, files)
}
for _, f := range files {
info, _ := os.Stat(f)
if info.Size() > 2000 {
t.Errorf("%s is %d bytes, over its bound", f, info.Size())
}
total += info.Size()
}
if total > int64(HandedKept+1)*2000 {
t.Errorf("the record holds %d bytes", total)
}
if v.Store.Rolled() == 0 {
t.Error("rolling was not counted")
}
if h, _ := v.Store.Handed(5); len(h) != 5 || h[0].To != "g14.flood" {
t.Errorf("the newest are not read back: %+v", h)
}
}