R-105 (decision 169): retire the never-built slim DR record fields (no writer, no reader); 05 §9/§11, 06 §3.5 corrected
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -87,9 +87,9 @@ func (h *Handler) handleClearRecoveryMode(w http.ResponseWriter, r *http.Request
|
||||
type reEnrollResponse struct {
|
||||
HostID string `json:"host_id"`
|
||||
APIKeyRotated bool `json:"api_key_rotated"`
|
||||
Directive json.RawMessage `json:"directive"` // non-secret DR directive
|
||||
KEscrowB64 string `json:"k_escrow_b64"` // opaque PBS-key escrow blob
|
||||
IdentityEscrowB64 string `json:"identity_escrow_b64"` // opaque identity escrow blob
|
||||
Directive json.RawMessage `json:"directive"` // always {} — the directive is retired (R-105, decision 169); kept for the wire shape
|
||||
KEscrowB64 string `json:"k_escrow_b64"` // opaque PBS-key escrow blob
|
||||
IdentityEscrowB64 string `json:"identity_escrow_b64"` // opaque identity escrow blob
|
||||
}
|
||||
|
||||
// handleReEnroll is the re-enroll handshake (slice 10D.2). Gated ONLY on RECOVERY MODE (the lost box
|
||||
@@ -136,9 +136,6 @@ func (h *Handler) handleReEnroll(w http.ResponseWriter, r *http.Request, hostID
|
||||
if bundle, err := h.store.GetHostDRBundle(hostID); err == nil && bundle != nil {
|
||||
resp.KEscrowB64 = base64.StdEncoding.EncodeToString(bundle.KEscrowBlob)
|
||||
resp.IdentityEscrowB64 = base64.StdEncoding.EncodeToString(bundle.IdentityBlob)
|
||||
if bundle.DirectiveJSON != "" {
|
||||
resp.Directive = json.RawMessage(bundle.DirectiveJSON)
|
||||
}
|
||||
}
|
||||
h.logger.Printf("[INFO] DR: host %s RE-ENROLLED (hub credential rotated; old key revoked; directive served)", hostID)
|
||||
// The new key is returned so the box can use it; the operator sees the rotation in the response.
|
||||
@@ -175,9 +172,6 @@ func (h *Handler) handleGetRestoreDirective(w http.ResponseWriter, r *http.Reque
|
||||
if bundle, err := h.store.GetHostDRBundle(hostID); err == nil && bundle != nil {
|
||||
resp.KEscrowB64 = base64.StdEncoding.EncodeToString(bundle.KEscrowBlob)
|
||||
resp.IdentityEscrowB64 = base64.StdEncoding.EncodeToString(bundle.IdentityBlob)
|
||||
if bundle.DirectiveJSON != "" {
|
||||
resp.Directive = json.RawMessage(bundle.DirectiveJSON)
|
||||
}
|
||||
}
|
||||
writeJSON(w, http.StatusOK, resp)
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ func TestRestoreDirective_GatedAndExpires(t *testing.T) {
|
||||
seedHost(t, st, "h1", "c1", "HKEY")
|
||||
// Seed a DR bundle: K-escrow row + identity blob + directive.
|
||||
st.SaveHostEscrow("h1", []byte("opaque-K-escrow"), "01:36:e9:…", "zero_knowledge", time.Now().UTC().Format(time.RFC3339), "")
|
||||
st.SaveHostDRBundle("h1", []byte("opaque-identity"), `{"pbs_repo":"r","tunnel_id":"t","expected_key_fingerprint":"01:36:e9:…"}`)
|
||||
st.SaveHostDRBundle("h1", []byte("opaque-identity"))
|
||||
|
||||
// Not in recovery mode → 403.
|
||||
if rr := do(h, http.MethodGet, "/hosts/h1/restore-directive", "HKEY", ""); rr.Code != http.StatusForbidden {
|
||||
|
||||
@@ -25,7 +25,7 @@ func seedEscrowedHost(t *testing.T, st *store.Store, hostID, customerID, apiKey
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(identity) > 0 {
|
||||
if err := st.SaveHostDRBundle(hostID, identity, `{}`); err != nil {
|
||||
if err := st.SaveHostDRBundle(hostID, identity); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ func TestReportACK_EscrowStatus(t *testing.T) {
|
||||
if _, _, err := st.SaveHostEscrow("hv1", []byte("k-blob"), "fp", "zero_knowledge", "2026-07-09T20:00:00Z", "abc123"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := st.SaveHostDRBundle("hv1", []byte("identity-blob"), "{}"); err != nil {
|
||||
if err := st.SaveHostDRBundle("hv1", []byte("identity-blob")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rr2 := do(h, http.MethodPost, "/report", globalKey, `{"customer_id":"cust-e"}`)
|
||||
|
||||
@@ -1220,9 +1220,9 @@ type escrowUploadRequest struct {
|
||||
KeyFingerprint string `json:"key_fingerprint"` // for operator display only
|
||||
Posture string `json:"posture"` // e.g. "zero_knowledge"
|
||||
CreatedAt string `json:"created_at"` // RFC3339
|
||||
// Slice 10D.1 — optional DR bundle, stored alongside the K-escrow (both opaque/non-secret).
|
||||
IdentityBlobB64 string `json:"identity_blob_b64,omitempty"` // age-wrapped {tunnel_token, pbs_token}
|
||||
DirectiveJSON json.RawMessage `json:"directive,omitempty"` // non-secret directive (pbs repo/ns, expected fp, tunnel id)
|
||||
// Slice 10D.1 — optional identity escrow, stored alongside the K-escrow (opaque). An old agent may still send a
|
||||
// `directive`; it is ignored (R-105, decision 169 — retired, never read).
|
||||
IdentityBlobB64 string `json:"identity_blob_b64,omitempty"` // age-wrapped {tunnel_token, pbs_token}
|
||||
// SLICE 3 — sha256 hex of the restic repo password sealed in the identity blob (non-reversible hash
|
||||
// of a 256-bit random secret — safe to store/serve; present only when a staged password was folded in).
|
||||
ResticPwSHA256 string `json:"restic_pw_sha256,omitempty"`
|
||||
@@ -1290,25 +1290,20 @@ 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 + the non-secret DR directive alongside
|
||||
// the K-escrow (both opaque / non-secret — no usable secret hub-side). Additive: a slice-7
|
||||
// upload without these is unchanged.
|
||||
// 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
|
||||
}
|
||||
directive := req.DirectiveJSON
|
||||
if len(directive) == 0 || !json.Valid(directive) {
|
||||
directive = json.RawMessage("{}")
|
||||
}
|
||||
if err := h.store.SaveHostDRBundle(pathHostID, idBlob, string(directive)); err != nil {
|
||||
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 + directive)", pathHostID, len(idBlob))
|
||||
h.logger.Printf("[INFO] stored DR bundle for host %s (identity %d bytes)", 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)
|
||||
|
||||
Reference in New Issue
Block a user