mesh-vault: secret_holders lists every record of the vault's store by name and fingerprint (issue 1409)

S1 vault-licence-secrets-listed. secret_holders answers `records` and
`recordsCount` beside holders, count and enrolments: each store record's name,
generation, since, changed, changedBy/changedVia, origin, length and the vault's
own fingerprint of the whole value, plus key and part for licence.<key>.<part>.
A record the vault cannot open stays listed with an error. Never a value.

Builds: design 39 bus-and-switch §20, §15; design 39 parts §12 (D1); design 24 Every secret lives in the vault, Beyond generate and hold; as-is 06 The vault, as it runs.
Lead answers (issue 1409 note 11): V1, V-thorough, V2-V8. V3 drops the map's
tokenFingerprint and AC5; V-thorough widens the licence.-prefix list to every record.
This commit is contained in:
2026-11-12 17:34:00 +01:00
parent 836fcd81f6
commit 52572698cb
4 changed files with 124 additions and 4 deletions
+4 -1
View File
@@ -257,7 +257,10 @@ runner's `ledgers` are never retired this way. A waiter on `rotated` or `own-rep
`secret_export`, `secret_holders`, `secret_verify` answer as the TypeScript vault's did, and its custody
ledger (`${dir:ledger}`, the same JSON files) is kept by a pass over the contributions file every minute.
`secret_handed` and `secret_vault_export` read the vault's own store. None returns a value.
`secret_handed` and `secret_vault_export` read the vault's own store. None returns a value. `secret_holders` also answers
`records`: every record the store keeps (a licence's two secrets included), by name, generation, when stored and
changed, who changed it, origin, length and fingerprint, never the value; a record the vault cannot open stays in the
list with an `error`.
**The ledger follows ADR 0230.** A contributions file that does not say who is asked for (unreadable, not
JSON, no `given` list or a null one, or for another requirement) is not "nobody": the pass records nothing,
@@ -10,6 +10,8 @@ import (
"errors"
"fmt"
"regexp"
"strings"
"time"
)
// LicencePutVerb is the verb that hands a licence's secret to the vault.
@@ -78,3 +80,47 @@ func (v *Vault) LicencePutTake(caller, call, sealed string) (Changed, error) {
}
return v.keepWith(c.secret, string(value), caller, "", OriginAccepted, keepOptions{generation: c.generation})
}
// RecordHeld is one record of the vault's store as secret_holders lists it (issue 1409, design 39 parts §12): by name
// and fingerprint, never the value. For a licence's secret it also says the licence's key and the part.
type RecordHeld struct {
Name string `json:"name"`
Key string `json:"key,omitempty"`
Part string `json:"part,omitempty"`
Generation int `json:"generation"`
Since time.Time `json:"since"`
Changed time.Time `json:"changed"`
ChangedBy string `json:"changedBy"`
ChangedVia string `json:"changedVia,omitempty"`
Origin string `json:"origin"`
Length int `json:"length"`
Fingerprint string `json:"fingerprint"`
// Error says why the vault could not open its copy; the record stays in the list.
Error string `json:"error,omitempty"`
}
// RecordsHeld lists every record the store keeps, by name, ordered as the store lists them. The fingerprint is the
// vault's own of the whole value, as the ledger gives it.
func (v *Vault) RecordsHeld() ([]RecordHeld, error) {
records, err := v.Store.List()
if err != nil {
return nil, err
}
out := make([]RecordHeld, 0, len(records))
for _, r := range records {
row := RecordHeld{Name: r.Name, Generation: r.Generation, Since: r.MadeAt, Changed: r.ChangedAt,
ChangedBy: r.ChangedBy, ChangedVia: r.ChangedVia, Origin: r.Origin}
if rest, ok := strings.CutPrefix(r.Name, "licence."); ok {
if i := strings.LastIndex(rest, "."); i > 0 {
row.Key, row.Part = rest[:i], rest[i+1:]
}
}
if value, err := openAtRest(r, v); err != nil {
row.Error = "the vault could not open its copy: " + err.Error()
} else {
row.Length, row.Fingerprint = len(value), Fingerprint(value)
}
out = append(out, row)
}
return out, nil
}
@@ -1,8 +1,11 @@
package main
import (
"encoding/json"
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/seal"
)
// putLicence hands one part of a licence to the vault in licence-put's two steps.
@@ -114,3 +117,54 @@ func TestATokenIsGivenOnlyToAMachineABindingNames(t *testing.T) {
t.Fatalf("the manager's grant: %q %v", got, err)
}
}
// issue 1409, design 39 bus-and-switch §20, parts §12: secret_holders lists every record of the store by name and
// fingerprint, a licence's two secrets with their key and part, a record it cannot open with an error, and no value.
func TestSecretHoldersListsEveryStoreRecordByNameAndFingerprintNeverTheValue(t *testing.T) {
v := newVault(t, nil)
const manager = "claude-licence-manager"
grant, access := `{"refreshToken":"rt-SECRET-grant"}`, `{"accessToken":"at-SECRET-access"}`
for part, value := range map[string]string{LicenceGrant: grant, LicenceAccess: access} {
if _, err := putLicence(t, v, manager, "jane_at_example.org", part, 3, value); err != nil {
t.Fatal(err)
}
}
if _, err := v.Put("pair/postgres/gitea", "other-SECRET-value", "the test"); err != nil {
t.Fatal(err)
}
broken, _, _ := v.Store.Read("pair/postgres/gitea")
broken.Name, broken.AtRest = "pair/broken/one", seal.Box{}
if err := v.Store.Write(broken); err != nil {
t.Fatal(err)
}
got, err := toolNamed(t, definitions(&Env{Vault: v.Vault}), "secret_holders").Run(map[string]any{})
if err != nil {
t.Fatal(err)
}
raw, _ := json.Marshal(got)
if strings.Contains(string(raw), "SECRET") {
t.Fatalf("the answer holds a value: %s", raw)
}
m := got.(map[string]any)
rows := map[string]RecordHeld{}
for _, r := range m["records"].([]RecordHeld) {
rows[r.Name] = r
}
if m["recordsCount"] != 4 || len(rows) != 4 {
t.Fatalf("records %v, want 4 (two licence secrets, a pair, a broken one)", rows)
}
for part, value := range map[string]string{LicenceGrant: grant, LicenceAccess: access} {
r := rows["licence.jane_at_example.org."+part]
if r.Key != "jane_at_example.org" || r.Part != part || r.Generation != 3 || r.ChangedBy != manager ||
r.Origin != OriginAccepted || r.Length != len(value) || r.Fingerprint != Fingerprint(value) || r.Error != "" {
t.Errorf("the %s secret's row is %+v", part, r)
}
}
if r := rows["pair/postgres/gitea"]; r.Key != "" || r.Fingerprint != Fingerprint("other-SECRET-value") {
t.Errorf("the pair's row is %+v", r)
}
if r := rows["pair/broken/one"]; r.Error == "" || r.Fingerprint != "" {
t.Errorf("a record the vault cannot open must stay listed with an error: %+v", r)
}
}
+20 -3
View File
@@ -380,15 +380,32 @@ func definitions(env *Env) []toolDef {
{Name: "secret_holders",
Description: "Who holds a secret from this vault: each consumer's login, node and module, when it was " +
"granted, how many times it has been rotated and when, and the fingerprint of the current value. " +
"Also every record the vault's own store keeps (`records`, with `recordsCount`): its name, generation, when " +
"it was stored and last changed, who changed it, its origin, its length and its fingerprint, and for a " +
"licence's secret (licence.<key>.grant, licence.<key>.access) the key and the part. " +
"Fingerprints only — the value is never returned.",
run: func(string, map[string]any) (any, error) {
enrolments := []EnrolmentHeld{}
records := []RecordHeld{}
recordsError := ""
if env.Vault != nil {
enrolments, _ = env.Vault.EnrolmentsHeld()
var err error
if records, err = env.Vault.RecordsHeld(); err != nil {
recordsError = "the vault could not list its store: " + err.Error()
records = []RecordHeld{}
}
}
stored := func(m map[string]any) map[string]any {
m["records"], m["recordsCount"] = records, len(records)
if recordsError != "" {
m["recordsError"] = recordsError
}
return m
}
if env.Ledger == nil {
return map[string]any{"holders": []any{}, "count": 0, "enrolments": enrolments,
"error": "the vault has no ledger (MESH_VAULT_LEDGER is not set)"}, nil
return stored(map[string]any{"holders": []any{}, "count": 0, "enrolments": enrolments,
"error": "the vault has no ledger (MESH_VAULT_LEDGER is not set)"}), nil
}
var asked []Contribution
if env.Receives != "" {
@@ -407,7 +424,7 @@ func definitions(env *Env) []toolDef {
}
holders = append(holders, row)
}
return map[string]any{"holders": holders, "count": len(holders), "enrolments": enrolments}, nil
return stored(map[string]any{"holders": holders, "count": len(holders), "enrolments": enrolments}), nil
}},
{Name: "secret_verify",
Description: "Check one holder's secret without seeing it: the fingerprint the vault recorded against the " +