hub R-1254 (P2): the escrow key, its hash and the identity blob in ONE transaction; a bad identity blob is refused before anything is stored. Committed, not deployed.
gates / gates (push) Successful in 13m27s

This commit is contained in:
2026-10-24 20:37:59 +02:00
parent 80f1ab3167
commit dcf4f84cb7
6 changed files with 165 additions and 16 deletions
+16 -15
View File
@@ -1409,10 +1409,23 @@ func (h *Handler) handleHostEscrowPut(w http.ResponseWriter, r *http.Request, pa
if createdAt == "" {
createdAt = time.Now().UTC().Format(time.RFC3339)
}
// Slice 10D.1: optionally the IDENTITY escrow blob alongside the K-escrow (opaque — no usable secret hub-side).
// R-1254: decoded BEFORE any write (a bad one used to 400 after the new key was already stored) and saved in the
// SAME transaction as the key and its hash — all of the new package or all of the old. The directive is retired
// (R-105).
var idBlob []byte
if req.IdentityBlobB64 != "" {
b, derr := base64.StdEncoding.DecodeString(req.IdentityBlobB64)
if derr != nil || len(b) == 0 {
http.Error(w, "Invalid payload: identity_blob_b64 not valid base64", http.StatusBadRequest)
return
}
idBlob = b
}
// Store the OPAQUE bytes. No decrypt path exists — the hub cannot open this. Part B (v0.60.0):
// when this upload supersedes a DIFFERENT-passphrase old blob, the old one is RETAINED (not
// overwritten) so its recovery-code-recoverable history survives (Viktor's data-first ruling).
superseded, prevPwSHA, serr := h.store.SaveHostEscrow(pathHostID, blob, req.KeyFingerprint, req.Posture, createdAt, req.ResticPwSHA256)
superseded, prevPwSHA, serr := h.store.SaveHostEscrowWithIdentity(pathHostID, blob, idBlob, req.KeyFingerprint, req.Posture, createdAt, req.ResticPwSHA256)
if serr != nil {
h.logger.Printf("[ERROR] Failed to store escrow for host %s: %v", pathHostID, serr)
http.Error(w, "Internal error", http.StatusInternalServerError)
@@ -1434,20 +1447,8 @@ func (h *Handler) handleHostEscrowPut(w http.ResponseWriter, r *http.Request, pa
h.maybeEmitRepoKeyChanged(host.CustomerID, pathHostID, prevPwSHA, req.ResticPwSHA256, n)
}
}
// Slice 10D.1: optionally store the IDENTITY escrow blob alongside the K-escrow (opaque — no usable secret
// hub-side). Additive: a slice-7 upload without it is unchanged. The directive is retired (R-105).
if req.IdentityBlobB64 != "" {
idBlob, derr := base64.StdEncoding.DecodeString(req.IdentityBlobB64)
if derr != nil || len(idBlob) == 0 {
http.Error(w, "Invalid payload: identity_blob_b64 not valid base64", http.StatusBadRequest)
return
}
if err := h.store.SaveHostDRBundle(pathHostID, idBlob); err != nil {
h.logger.Printf("[ERROR] Failed to store DR bundle for host %s: %v", pathHostID, err)
http.Error(w, "Internal error", http.StatusInternalServerError)
return
}
h.logger.Printf("[INFO] stored DR bundle for host %s (identity %d bytes)", pathHostID, len(idBlob))
if idBlob != nil {
h.logger.Printf("[INFO] stored DR bundle for host %s (identity %d bytes, same transaction as the key — R-1254)", pathHostID, len(idBlob))
}
h.logger.Printf("[INFO] stored opaque escrow blob for host %s (%d bytes, posture=%s, fp=%s)",
pathHostID, len(blob), req.Posture, req.KeyFingerprint)