mesh-vault: ADR 0349's vault tests (issue 674)
mesh/merge-gate head 84d8198f1540 merged into trunk 8aa37e8b835d — pass: the change touches no module of the mesh's graph
mesh/repo-check head 84d8198f1540 merged into trunk 8aa37e8b835d — fail: its merge-check.sh failed: files asking for a setting without saying whether it is…
mesh/delivery-group group pkg/57-v1-vault-in-go rejected: a member's own check failed
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate head 84d8198f1540 merged into trunk 8aa37e8b835d — pass: the change touches no module of the mesh's graph
mesh/repo-check head 84d8198f1540 merged into trunk 8aa37e8b835d — fail: its merge-check.sh failed: files asking for a setting without saying whether it is…
mesh/delivery-group group pkg/57-v1-vault-in-go rejected: a member's own check failed
mesh/delivery superseded: a newer head of the same pull request
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,503 @@
|
||||
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[:])
|
||||
}
|
||||
|
||||
// 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"))
|
||||
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(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).
|
||||
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)
|
||||
}
|
||||
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)
|
||||
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.
|
||||
if _, err := v.SetOperatorKey(keys.PrivateKey, "the test"); err == nil {
|
||||
t.Error("the vault took a private key as 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(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"))
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Until who may ask is decided by a node's declaration and a module's own account (V4, P3), the vault
|
||||
// that runs answers nobody.
|
||||
func TestTheRunningVaultAnswersNobodyYet(t *testing.T) {
|
||||
v := newVault(t, nil) // what main builds: no list
|
||||
if _, err := v.Put("pair/postgres/gitea", "x", "the test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
asker, _ := seal.GenerateKeyPair()
|
||||
for _, caller := range []string{"g14.node-provisioner", "novox.mesh-provisioner", "controller", "person.jochen", "novox.node-tools"} {
|
||||
if _, err := v.Get(caller, "pair/postgres/gitea", asker.PublicKey); err == nil {
|
||||
t.Errorf("the running vault answered %s", caller)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b, err := OwnKey(dir)
|
||||
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())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user