v0.129.0 — a correct code for an earlier package stops being called wrong (R-311)
gates / gates (push) Successful in 14s
gates / gates (push) Successful in 14s
Yesterday's drill proved a retained escrow package opens a set-aside store and restores planted files byte-identical, while this agent answered the customer's correct code with "the recovery code did not open the sealed bundle". Nothing had ever tried the retained packages, so a correct-but-earlier code and a mistype were genuinely indistinguishable. OffsiteKeyRecoverer gains an optional FetchRetained, consulted ONLY after the current package refuses, so the ordinary recovery pays nothing for it and cannot fail because of it. A match returns ErrCodeOpensRetained wrapped in a RetainedOpenedError carrying the supersession date - no material, no code, no password. The local API answers 422: a FIFTH status added to the R-224 switch, never a restructuring of it. Fail-safe in every direction. Nil fetcher, a hub too old for the route (404 is a clean "none"), a transport failure, a malformed package: each leaves the original refusal standing. Attempts bounded at 6 because each unwrap is ~1s of scrypt. Seven tests with REAL age crypto - the two situations are indistinguishable AT THE UNWRAP, so a faked unwrap would prove nothing. Red-proof asserted applied: remove the retained lookup and the fail-closed wrong-code error returns, which is the lie in those exact words.
This commit is contained in:
@@ -354,3 +354,69 @@ func (c *Client) FetchIdentityEscrow(ctx context.Context) (*IdentityEscrowRespon
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// RetainedEscrowPackage is one RETAINED (superseded) sealed identity package. The blob is ciphertext
|
||||
// and is useless without R. `SupersededAt` is the only thing here a human ever sees — it is what lets
|
||||
// the recovery screen name WHICH earlier package a code belongs to.
|
||||
type RetainedEscrowPackage struct {
|
||||
Index int `json:"index"`
|
||||
SupersededAt string `json:"superseded_at"`
|
||||
KeyFingerprint string `json:"key_fingerprint"`
|
||||
IdentityEscrowB64 string `json:"identity_escrow_b64"`
|
||||
}
|
||||
|
||||
// RetainedEscrowResponse mirrors GET /api/v1/hosts/{host_id}/escrow/retained (hub >= v0.103.0, R-311).
|
||||
//
|
||||
// UnopenableCount is NOT noise. It counts retained packages the hub holds whose key material is absent
|
||||
// (every pre-v0.93.0 row): on a box with those and nothing else, a perfectly correct old recovery code
|
||||
// opens nothing, and the reason is a defect of ours. A caller that ignores this number will tell such a
|
||||
// customer their code is wrong — the exact failure this whole chain exists to stop.
|
||||
type RetainedEscrowResponse struct {
|
||||
HostID string `json:"host_id"`
|
||||
Count int `json:"count"`
|
||||
UnopenableCount int `json:"unopenable_count"`
|
||||
TruncatedCount int `json:"truncated_count"`
|
||||
Packages []RetainedEscrowPackage `json:"packages"`
|
||||
}
|
||||
|
||||
// FetchRetainedIdentityEscrow reads back THIS host's RETAINED sealed identity packages (R-311 —
|
||||
// the retained siblings of FetchIdentityEscrow, self-scoped server-side by the same per-host key).
|
||||
//
|
||||
// SEPARATE FROM FetchIdentityEscrow ON PURPOSE. The ordinary recovery must not pay for this call, and
|
||||
// must not fail because of it: the current package is tried first and alone, and this is reached only
|
||||
// after that has refused. A hub too old to know this route answers 404, which is a CLEAN "none" here
|
||||
// and must never be reported as a failed recovery.
|
||||
func (c *Client) FetchRetainedIdentityEscrow(ctx context.Context) (*RetainedEscrowResponse, error) {
|
||||
if c.hostID == "" {
|
||||
return nil, fmt.Errorf("hub: FetchRetainedIdentityEscrow requires a configured host_id")
|
||||
}
|
||||
url := c.baseURL + "/api/v1/hosts/" + c.hostID + "/escrow/retained"
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("hub: building retained-escrow request: %w", err)
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+c.apiKey)
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
resp, err := c.hc.Do(req)
|
||||
if err != nil {
|
||||
return nil, &TransportError{Err: err}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
raw, _ := io.ReadAll(io.LimitReader(resp.Body, 4<<20))
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
// A hub older than v0.103.0 has no such route. That is "no retained packages", not a fault —
|
||||
// returning an error here would turn an old hub into a failed recovery on a box whose current
|
||||
// package simply did not open.
|
||||
return &RetainedEscrowResponse{HostID: c.hostID}, nil
|
||||
}
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return nil, &HTTPError{StatusCode: resp.StatusCode, BodyTail: tail(raw, 256)}
|
||||
}
|
||||
var out RetainedEscrowResponse
|
||||
if err := json.Unmarshal(raw, &out); err != nil {
|
||||
return nil, fmt.Errorf("hub: decoding retained escrow fetch: %w", err)
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user