R-241 part 2: the comparison the box already makes becomes the thing that offers recovery
THE FACT WAS COMPUTED EVERY CYCLE AND KEPT NOWHERE. EscrowAutoConfirmer.Reconcile
has compared the hub's restic_pw_sha256 against the local key on every ACK since
SLICE 3. On the final-walk venue it logged, at 03:28:03Z and thirty-five minutes
before the customer looked, "the hub's escrow blob does not cover the CURRENT repo
password (hub hash 30ef574f != local 9b4a9a9d)" - and dropped it. The recovery
screen, evaluating in the same process, went on asking a question that could not
see it.
Now persisted: settings.HubEscrowKeySHA256 + HubEscrowKeyCheckedAt, recorded
UNCONDITIONALLY in Reconcile beside RecordPresence and RecordSuperseded - same
place, same reason: the box that needs it most is the rebuilt one with no target,
on which every gate below returns early.
OffsiteRecoveryOffer gains SHAPE (c): the hub holds a package for a key OTHER than
the one we are using. (a) and (b) are both proxies for that question and both have
now been wrong in opposite directions - (a) goes false the moment anything mints,
(b) is unreachable while the escrow is pending.
SEC 7.2, decided deliberately and stated in the code:
- a KNOWN DIFFERENCE offers, however old the reading. Age is not gated on. Both
sides are local; only the hub's half can be stale, and what the hub holds does
not change without a ceremony THIS box runs, which refreshes the hash on the
next ACK. Gating on age would make a box offline from the hub silently stop
offering - the exact failure this session removes. CheckedAt is persisted for
diagnosis, not as a gate.
- an ABSENT hash falls back to (a)/(b) and does NOT offer. "" is the hub
positively saying its package seals no repository password (legacy hash-less
escrow). Nothing to compare, and offering would put a permanent screen in
front of every legacy box.
The write damper: CheckedAt refreshes on every ack carrying a hash, but a save is
skipped when both the hash and the UTC day are unchanged, so an idle box does not
rewrite settings.json every fifteen minutes. It records WHEN WE LAST HEARD, not
when it last changed - the R-100 distinction.
Tests: Scenario C (a differing key offers, with both proxies asserted false first),
Scenario D (a matching key offers nothing), fact 1 still required, shape (a) still
works, and both SEC 7.2 halves.
RED-PROOFS, each with the mutation confirmed present in the file first:
D) hubHash != localHash conjunct dropped -> Scenario D FAILS (a healthy box
offered recovery forever); Scenario C still passes
WIRING) RecordEscrowKeyHash removed from the EscrowAutoConfirmer literal in
main.go -> TestMainWiresRecordEscrowKeyHash FAILS. This is the ships-inert
shape: unwired, everything compiles, every test in the package passes, the
auto-confirm still works, and shape (c) reads an empty hash forever.
Green: go build, go vet, go test ./... all pass.
This commit is contained in:
@@ -1522,13 +1522,49 @@ func (m *Manager) needsOffsiteCredential(t *settings.OffboxTarget) bool {
|
||||
//
|
||||
// Scenario B still holds exactly: a healthy box has its own password and is not orphaned; a box that
|
||||
// never had off-site backups fails fact 1; an unclaimed box never reaches an authenticated page.
|
||||
// ── SHAPE (c), v0.206.0, R-241 — THE DISCRIMINATOR THAT ANSWERS THE REAL QUESTION ───────────────
|
||||
//
|
||||
// Shapes (a) and (b) are both PROXIES for one question — *does the hub hold a package for a key other
|
||||
// than the one I am using?* — and both have now been wrong, in opposite directions:
|
||||
//
|
||||
// - (a) "no repository password" went false the moment anything minted one. Before v0.206.0's mint
|
||||
// guard that happened by itself, ~30 minutes after a rebuild, and the customer who logged in the
|
||||
// next morning never saw the screen. That is R-241.
|
||||
// - (b) "a run proved the repo will not open" is unreachable on exactly that box: the only producer
|
||||
// of RepoState=="orphaned" is ensureOffboxRepo, which is downstream of the escrow gate in
|
||||
// runOffboxBackup, and the escrow can never confirm while the hub's package covers a different
|
||||
// key. Self-locking.
|
||||
//
|
||||
// (c) asks the question directly, from two facts the box already holds: the hash the hub's package
|
||||
// covers (ACK-cached) and the hash of the key on disk. **This comparison was already computed on every
|
||||
// ACK and thrown away** — see settings.HubEscrowKeySHA256.
|
||||
//
|
||||
// ⚠ §7.2 — WHAT A STALE OR ABSENT READING RESOLVES TO, decided deliberately rather than by default:
|
||||
//
|
||||
// - **A KNOWN DIFFERENCE OFFERS, however old the reading.** Age is not gated on. Both sides of the
|
||||
// comparison are local; only the hub's half can be stale, and what the hub holds does not change
|
||||
// without a ceremony THIS BOX runs — which refreshes the hash on the next ACK. Gating on age would
|
||||
// add a second failure mode (a box offline from the hub silently stops offering) to fix a window
|
||||
// that closes itself. `HubEscrowKeyCheckedAt` is persisted for diagnosis, not as a gate.
|
||||
// - **AN ABSENT HASH FALLS BACK TO (a)/(b), it does not offer.** "" is what the hub sends for a
|
||||
// legacy hash-less package — one that provably seals no repository password. There is nothing for
|
||||
// (c) to compare, and offering on it would put a permanent screen in front of every legacy box.
|
||||
// This is the one place where "not knowing" resolves to silence, and it does so because an empty
|
||||
// hash is not an unknown: it is the hub positively saying the package covers no key.
|
||||
//
|
||||
// So: fail-closed (offer) on a known difference; fall back on a hash never learned. Pinned by
|
||||
// TestR241_ScenarioD_* and TestR241_StaleComparison_*.
|
||||
func (m *Manager) OffsiteRecoveryOffer() bool {
|
||||
if m.settings == nil || !m.settings.GetHubEscrowIdentityPresent() {
|
||||
return false // the hub holds nothing for us — nothing to recover
|
||||
}
|
||||
if _, ok := m.OffboxRepoPasswordHash(); !ok {
|
||||
localHash, hasLocal := m.OffboxRepoPasswordHash()
|
||||
if !hasLocal {
|
||||
return true // (a) no repository password at all — the pristine rebuilt box
|
||||
}
|
||||
if hubHash, _ := m.settings.GetHubEscrowKeySHA256(); hubHash != "" && hubHash != localHash {
|
||||
return true // (c) the hub's package covers a DIFFERENT key than the one we are using
|
||||
}
|
||||
return m.OffboxOrphaned() // (b) a password exists but the inherited history will not open under it
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user