diff --git a/modules/mesh-vault/cmd/mesh-vault/store.go b/modules/mesh-vault/cmd/mesh-vault/store.go index 192c60fe..3ed40ebc 100644 --- a/modules/mesh-vault/cmd/mesh-vault/store.go +++ b/modules/mesh-vault/cmd/mesh-vault/store.go @@ -39,6 +39,12 @@ type Record struct { MadeAt time.Time `json:"made-at"` ChangedAt time.Time `json:"changed-at"` ChangedBy string `json:"changed-by"` + // ChangedVia is the caller the runtime proved, when it is not who ChangedBy names: the controller + // carrying a person's act (design 57's V2). Empty when they are the same. + ChangedVia string `json:"changed-via,omitempty"` + // Origin is where the value came from: made by the vault, accepted at the controller's terminal, typed at + // a desk, or issued by a provider's software (ADR 0349 clauses 8 to 10). Empty is made, as V1 wrote it. + Origin string `json:"origin,omitempty"` } // OperatorCopy is a value sealed to the operator key named in Key. @@ -58,6 +64,8 @@ type Store struct { handedMu sync.Mutex rolled int + // asksMu serialises the record of asks, so two asks for one secret at once do not both pass its bounds. + asksMu sync.Mutex } // The record of hand-outs is bounded: rolled at HandedMaxDefault into handed.jsonl.1 … .HandedKept, the @@ -160,6 +168,7 @@ func (s *Store) RecordHanding(h Handing) (rolled bool, err error) { if err := s.roll(path); err != nil { return false, err } + s.rolled++ rolled = true } return rolled, appendSynced(path, append(raw, '\n')) @@ -176,7 +185,6 @@ func (s *Store) roll(path string) error { if err := os.Rename(path, path+".1"); err != nil { return err } - s.rolled++ return syncDir(s.dir) } @@ -334,3 +342,99 @@ func syncDir(dir string) error { defer d.Close() return d.Sync() } + +// The two lines an ask leaves in the record: when it opened, and how it ended. +const ( + AskOpened = "opened" + AskEnded = "ended" +) + +// AskRecord is one line of the record of asks for a module's own secret at a desk (ADR 0277 §25, in the vault's +// store since ADR 0349 clause 9): who asked, as the bus named them, for what, at which desk, and how it ended. +// Never a value. +type AskRecord struct { + ID string `json:"id"` + At time.Time `json:"at"` + Event string `json:"event"` + Secret string `json:"secret"` + Node string `json:"node,omitempty"` + Module string `json:"module,omitempty"` + Name string `json:"name,omitempty"` + Desk string `json:"desk,omitempty"` + AskedBy string `json:"asked_by,omitempty"` + Caller string `json:"caller,omitempty"` + Outcome string `json:"outcome,omitempty"` +} + +// RecordAsk appends one line to the record of asks, synced before it returns; rolled as the record of +// hand-outs is, and it says whether it was. +func (s *Store) RecordAsk(r AskRecord) (rolled bool, err error) { + raw, err := json.Marshal(r) + if err != nil { + return false, err + } + path := filepath.Join(s.dir, "asks.jsonl") + if info, err := os.Stat(path); err == nil && info.Mode().IsRegular() && info.Size()+int64(len(raw))+1 > s.HandedBound() { + if err := s.roll(path); err != nil { + return false, err + } + rolled = true + } + return rolled, appendSynced(path, append(raw, '\n')) +} + +// asks is every line of the record of asks, oldest first, the rolled files before the current one. +func (s *Store) asks() ([]AskRecord, error) { + path := filepath.Join(s.dir, "asks.jsonl") + var out []AskRecord + for i := HandedKept; i >= 0; i-- { + file := path + if i > 0 { + file = fmt.Sprintf("%s.%d", path, i) + } + raw, err := os.ReadFile(file) + if errors.Is(err, os.ErrNotExist) { + continue + } + if err != nil { + return nil, err + } + for _, line := range strings.Split(strings.TrimSpace(string(raw)), "\n") { + var r AskRecord + if line != "" && json.Unmarshal([]byte(line), &r) == nil { + out = append(out, r) + } + } + } + return out, nil +} + +// AsksSince is every line of the record of asks at or after since, oldest first. +func (s *Store) AsksSince(since time.Time) ([]AskRecord, error) { + all, err := s.asks() + if err != nil { + return nil, err + } + var out []AskRecord + for _, r := range all { + if !r.At.Before(since) { + out = append(out, r) + } + } + return out, nil +} + +// Asks is the latest lines of the record of asks, newest first, at most limit. +func (s *Store) Asks(limit int) ([]AskRecord, error) { + s.asksMu.Lock() + defer s.asksMu.Unlock() + all, err := s.asks() + if err != nil { + return nil, err + } + out := []AskRecord{} + for i := len(all) - 1; i >= 0 && len(out) < limit; i-- { + out = append(out, all[i]) + } + return out, nil +} diff --git a/modules/mesh-vault/cmd/mesh-vault/vault.go b/modules/mesh-vault/cmd/mesh-vault/vault.go index 2ba4ec53..429978d2 100644 --- a/modules/mesh-vault/cmd/mesh-vault/vault.go +++ b/modules/mesh-vault/cmd/mesh-vault/vault.go @@ -35,6 +35,9 @@ type Vault struct { Log func(format string, args ...any) mu sync.Mutex + // callsMu guards pending: the keys of the calls open now (intake.go), never written anywhere. + callsMu sync.Mutex + pending map[string]*callKey } // Changed is what one change to a secret was. Never its value. @@ -65,6 +68,12 @@ func (v *Vault) log(format string, args ...any) { // Refused: a private key (it stays where it was made), and any value while the vault has no operator key — // a secret only the vault could open is what ADR 0113 §22 rules out. func (v *Vault) Put(name, value, by string) (Changed, error) { + return v.keep(name, value, by, "", OriginMade) +} + +// keep is Put with where the value came from (origin) and the caller the runtime proved when it is not who +// by names (via). +func (v *Vault) keep(name, value, by, via, origin string) (Changed, error) { if err := validName(name); err != nil { return Changed{}, err } @@ -104,9 +113,12 @@ func (v *Vault) Put(name, value, by string) (Changed, error) { return Changed{}, err } now := v.Now().UTC() + if via == by { + via = "" + } rec := Record{Name: name, Generation: 1, AtRest: atRest, Operator: OperatorCopy{Box: OperatorBox, Key: operator, Sealed: forOperator}, - MadeAt: now, ChangedAt: now, ChangedBy: by} + MadeAt: now, ChangedAt: now, ChangedBy: by, ChangedVia: via, Origin: origin} if held { rec.Generation, rec.MadeAt = before.Generation+1, before.MadeAt } diff --git a/modules/mesh-vault/module.json b/modules/mesh-vault/module.json index 29c631f5..d47bb29d 100644 --- a/modules/mesh-vault/module.json +++ b/modules/mesh-vault/module.json @@ -112,6 +112,7 @@ "secret_holders", "secret_verify", "secret_handed", + "secret_asks", "secret_vault_export", "provisioner_retirement", "provisioner_retire_approve", @@ -124,7 +125,11 @@ "name": "mesh-vault", "scope": "mesh", "serves": [ - "get" + "get", + "accept", + "secret-ask", + "issued", + "rotate" ] } ]