R-196 / R-204 item 2: a re-issue no longer marks a healthy escrow stale (hub v0.95.0)

ReissueCredentials marked the escrow stale on every re-issue, on precautionary
grounds — the box's re-apply MIGHT mint a fresh repository password. It usually
does not. A stale flag withholds restic_pw_sha256 from the ACK, which stops the
controller's auto-confirm, which leaves EscrowState pending, which makes
OffboxRunnable false: every off-site backup refused on a box whose key was never
in doubt — and the customer told to re-run the one ceremony that would have
superseded the key just recovered.

The case it guessed at is measured elsewhere: the controller's Scenario-F
re-check compares the sealed hash against the live repo password on every ACK
(and the mark was BLINDING it by emptying that hash), and R-197's
offsite_repo_key_changed fires on a proven difference across a supersession.

offsite_reissued is unchanged. MarkEscrowStale is kept without a caller so a
future EVIDENTIAL writer has the mechanism, with a test pinning it live.
TestReissue_InvalidatesEscrow is replaced by its exact inverse.
This commit is contained in:
2026-08-05 07:17:29 +02:00
parent 2a7ac03c47
commit d1a8edb332
3 changed files with 130 additions and 63 deletions
+32 -18
View File
@@ -445,14 +445,14 @@ func (s *Store) migrate() error {
// instead of trusting blob-presence. NULL/'' = a legacy or password-less blob (never auto-confirms).
s.db.Exec(`ALTER TABLE host_escrow ADD COLUMN restic_pw_sha256 TEXT`)
// v0.57.0 (2.3, escrow honesty on offsite re-issue) — stale_at is set when the offsite PROVIDER
// credentials are re-issued. ⚠ CORRECTED 2026-08-04 (R-196): it used to say "when the offsite repo
// password is re-issued", which nothing in the hub does — the repository password is generated on
// the box and never leaves it except sealed under R. The flag is PRECAUTIONARY (the box's re-apply
// MAY mint a fresh repository password), not evidence that it changed; the evidential signal is
// R-197's offsite_repo_key_changed. While set, the hub stops advertising "ceremony done" and
// withholds the possibly-non-matching restic_pw_sha256 from the auto-confirm ACK. NULL = current;
// a fresh ceremony (SaveHostEscrow) clears it.
// v0.57.0 (2.3, escrow honesty on offsite re-issue) — stale_at marks an escrow blob that may no
// longer cover the box's live repository password. While set, the hub stops advertising "ceremony
// done" and withholds restic_pw_sha256 from the auto-confirm ACK. NULL = current; a fresh ceremony
// (SaveHostEscrow) clears it.
// ⚠ NOTHING SETS IT as of v0.95.0 (R-196 / R-204 item 2). The only writer was the PRECAUTIONARY
// mark on offsite re-issue, which guessed rather than measured and so blocked off-site backups on
// boxes whose key had not changed. The column and its readers stay; see MarkEscrowStale for what a
// legitimate future writer would have to prove first.
s.db.Exec(`ALTER TABLE host_escrow ADD COLUMN stale_at DATETIME`)
// dr_recipe (SPIKE-dr-recipe-2026-06-16): the secret-free DR reconstruction recipe, stored
@@ -2699,12 +2699,24 @@ func (s *Store) CountCurrentEscrowWithIdentity() (total, withIdentity int, err e
return total, withIdentity, err
}
// MarkEscrowStale flags a host's escrow blob as stale (v0.57.0, 2.3) — called when the offsite
// PROVIDER credentials are re-issued. ⚠ CORRECTED 2026-08-04 (R-196): it used to say "when the offsite
// repo password is re-issued", which no hub path does; see the reasoning at offsite.ReissueCredentials.
// The flag is precautionary — the box's re-apply may mint a fresh repository password — not a
// measurement that one did. No-op when no escrow row exists; idempotent (only stamps the first
// re-issue since the last ceremony; a fresh ceremony clears stale_at via SaveHostEscrow's ON CONFLICT).
// MarkEscrowStale flags a host's escrow blob as stale (v0.57.0, 2.3).
//
// ⚠ IT HAS NO CALLER as of hub v0.95.0 (R-196 / R-204 item 2), and that is deliberate, not an
// oversight. Its ONE caller was `offsite.ReissueCredentials`, which called it on every re-issue on
// the PRECAUTIONARY grounds that the box's re-apply might mint a fresh repository password. It
// usually does not, so the call marked healthy escrows stale — and because a stale flag WITHHOLDS
// restic_pw_sha256 from the ACK (GetEscrowStatusForCustomer, below), it blocked every off-site backup
// on those boxes and asked the customer for a ceremony that would supersede a perfectly good key. The
// full reasoning, and the two measured signals that cover the real case, are at
// offsite.ReissueCredentials.
//
// KEPT, not deleted, because the FLAG is still live and correct — `stale_at` is read by the ACK, the
// operator card and the PBS-DR view, and a future EVIDENTIAL caller (one that has measured a key
// change rather than guessed at one) is the right way to set it. Pinned by
// TestReissue_DoesNotMarkAHealthyEscrowStale: if a caller reappears without that evidence, it fails.
//
// No-op when no escrow row exists; idempotent (only stamps the first mark since the last ceremony; a
// fresh ceremony clears stale_at via SaveHostEscrow's ON CONFLICT).
func (s *Store) MarkEscrowStale(hostID string) error {
_, err := s.db.Exec(`UPDATE host_escrow SET stale_at = datetime('now') WHERE host_id = ? AND stale_at IS NULL`, hostID)
return err
@@ -2733,11 +2745,13 @@ type EscrowStatus struct {
IdentityBlobPresent bool `json:"identity_blob_present"`
ResticPwSHA256 string `json:"restic_pw_sha256,omitempty"`
CreatedAt string `json:"created_at,omitempty"`
// Stale (v0.57.0, 2.3) — true when the offsite PROVIDER credentials were re-issued after the blob
// was sealed. ⚠ CORRECTED 2026-08-04 (R-196): it used to say "the offsite password was re-issued",
// which reads as the repository password and is not what happens; see MarkEscrowStale.
// Stale (v0.57.0, 2.3) — true when `stale_at` is stamped on the host's escrow row. ⚠ SINCE hub
// v0.95.0 (R-196 / R-204 item 2) NOTHING STAMPS IT: the one caller was the precautionary
// re-issue mark, and it was removed for marking healthy escrows stale. See MarkEscrowStale.
// When stale the ResticPwSHA256 is WITHHELD (emptied) so the controller cannot auto-confirm against
// a hash that may no longer match the live repo password — the ceremony must run again.
// a hash that may no longer match the live repo password — the ceremony must run again. That
// withholding is exactly why the precautionary caller had to go: it BLINDED the controller's own
// hash comparison, which is the measurement that actually detects a changed repository password.
Stale bool `json:"escrow_stale,omitempty"`
}