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:
@@ -32,7 +32,7 @@ func TestSaveHostEscrow_RetainsIdentityBlob(t *testing.T) {
|
||||
if _, _, err := st.SaveHostEscrow(h, []byte("k-blob-old"), "fp-old", "zk", "2026-07-09T00:00:00Z", "SHA_OLD"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := st.SaveHostDRBundle(h, oldIdentity, `{"gen":1}`); err != nil {
|
||||
if err := st.SaveHostDRBundle(h, oldIdentity); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ func TestSaveHostEscrow_RetainsIdentityBlob(t *testing.T) {
|
||||
if prev != "SHA_OLD" {
|
||||
t.Fatalf("prevResticPwSHA256 = %q, want SHA_OLD (R-197 needs the replaced hash)", prev)
|
||||
}
|
||||
if err := st.SaveHostDRBundle(h, newIdentity, `{"gen":2}`); err != nil {
|
||||
if err := st.SaveHostDRBundle(h, newIdentity); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ func TestDeleteHost_DemotesIdentityBlob(t *testing.T) {
|
||||
if _, _, err := s.SaveHostEscrow(hostID, []byte("k-blob"), "fp", "zk", "2026-07-16T00:00:00Z", "SHA_A"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := s.SaveHostDRBundle(hostID, identity, `{}`); err != nil {
|
||||
if err := s.SaveHostDRBundle(hostID, identity); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ func TestCountCurrentEscrowWithIdentity(t *testing.T) {
|
||||
if _, _, err := st.SaveHostEscrow("with-id", []byte("k"), "fp", "zk", "2026-07-16T00:00:00Z", "SHA1"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := st.SaveHostDRBundle("with-id", []byte("age-blob"), `{}`); err != nil {
|
||||
if err := st.SaveHostDRBundle("with-id", []byte("age-blob")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, _, err := st.SaveHostEscrow("without-id", []byte("k"), "fp", "zk", "2026-07-16T00:00:00Z", "SHA2"); err != nil {
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"os"
|
||||
"regexp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// R-105 option A (`09` §3 decision 169): the two "slim DR record" fields are retired. Nothing writes
|
||||
// `host_escrow.directive_json` any more (an escrow upload left a hand-made directive overwritten with `{}`), and
|
||||
// nothing reads `hosts.dr_record_json` or the directive. The columns STAY (a column nobody reads is harmless,
|
||||
// and dropping one is a schema change the hub's database backups would have to follow).
|
||||
|
||||
// COMPANION RED-PROOF (observed): on the pre-R-105 code SaveHostDRBundle overwrote the column — this failed with
|
||||
// "the identity-blob save must leave directive_json alone; got {}". Restored.
|
||||
func TestR105_IdentitySaveLeavesDirectiveColumnAlone(t *testing.T) {
|
||||
st := newResetStore(t)
|
||||
if err := st.UpsertHost(&Host{HostID: "h1", CustomerID: "c1", APIKey: "k"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, _, err := st.SaveHostEscrow("h1", []byte("K"), "fp", "zero_knowledge", "2026-10-07T00:00:00Z", "sha"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := st.db.Exec(`UPDATE host_escrow SET directive_json = '{"hand":"made"}' WHERE host_id = 'h1'`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := st.SaveHostDRBundle("h1", []byte("ID")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var got string
|
||||
if err := st.db.QueryRow(`SELECT directive_json FROM host_escrow WHERE host_id = 'h1'`).Scan(&got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != `{"hand":"made"}` {
|
||||
t.Fatalf("the identity-blob save must leave directive_json alone; got %s", got)
|
||||
}
|
||||
b, err := st.GetHostDRBundle("h1")
|
||||
if err != nil || b == nil || string(b.IdentityBlob) != "ID" || string(b.KEscrowBlob) != "K" {
|
||||
t.Fatalf("the bundle must still carry both blobs; got %+v err=%v", b, err)
|
||||
}
|
||||
}
|
||||
|
||||
// No reader: outside the schema statements, the column names do not appear in the store's source, so a new reader
|
||||
// cannot appear without this design being revisited.
|
||||
// COMPANION RED-PROOF (observed): with `DRRecordJSON` still scanned this failed naming `dr_record_json` in the
|
||||
// hosts select list. Restored.
|
||||
func TestR105_RetiredColumnsHaveNoReader(t *testing.T) {
|
||||
src, err := os.ReadFile("store.go")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
schema := regexp.MustCompile(`(?m)^\s*dr_record_json\s+TEXT NOT NULL DEFAULT '\{\}',$|ALTER TABLE host_escrow ADD COLUMN directive_json`)
|
||||
rest := schema.ReplaceAllString(string(src), "")
|
||||
for _, name := range []string{"dr_record_json", "directive_json", "DRRecordJSON", "DirectiveJSON"} {
|
||||
if regexp.MustCompile(`\b` + name + `\b`).MatchString(stripComments(rest)) {
|
||||
t.Errorf("%s is still read or written in store.go outside its schema statement", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func stripComments(s string) string {
|
||||
return regexp.MustCompile(`(?m)//.*$`).ReplaceAllString(s, "")
|
||||
}
|
||||
+18
-24
@@ -2792,7 +2792,6 @@ type Host struct {
|
||||
LastReportAt *time.Time
|
||||
DesiredJSON string
|
||||
DesiredGeneration int64
|
||||
DRRecordJSON string
|
||||
RecoveryModeUntil *time.Time // slice 10D: recovery mode active until this time (nil/past = off)
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
@@ -2864,7 +2863,7 @@ func scanHostRaw(scan func(dest ...any) error) (*Host, error) {
|
||||
var lastReport, recoveryUntil sql.NullString
|
||||
var createdAt, updatedAt string
|
||||
err := scan(&h.HostID, &h.CustomerID, &h.APIKey, &h.AgentVersion, &lastReport,
|
||||
&h.DesiredJSON, &h.DesiredGeneration, &h.DRRecordJSON, &recoveryUntil, &createdAt, &updatedAt)
|
||||
&h.DesiredJSON, &h.DesiredGeneration, &recoveryUntil, &createdAt, &updatedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -2882,7 +2881,7 @@ func scanHostRaw(scan func(dest ...any) error) (*Host, error) {
|
||||
}
|
||||
|
||||
const hostSelectCols = `host_id, customer_id, api_key, agent_version, last_report_at,
|
||||
desired_json, desired_generation, dr_record_json, recovery_mode_until, created_at, updated_at`
|
||||
desired_json, desired_generation, recovery_mode_until, created_at, updated_at`
|
||||
|
||||
// GetHostByAPIKey looks up a host by its per-host hub key. Returns nil (no error)
|
||||
// if no match — parallels GetCustomerConfigByAPIKey.
|
||||
@@ -3170,7 +3169,7 @@ func (s *Store) LatestHostDeletion(customerID string) (*HostDeletion, error) {
|
||||
// UpsertHost creates or updates a host identity (used by the admin mint). On
|
||||
// conflict it updates only operator-settable identity fields + updated_at; it does
|
||||
// NOT touch the reality columns (agent_version/last_report_at) or the inert intent
|
||||
// columns (desired_*/dr_record_json) — those are owned elsewhere.
|
||||
// columns (desired_*) — those are owned elsewhere.
|
||||
func (s *Store) UpsertHost(h *Host) error {
|
||||
if h.SecretsUnreadable {
|
||||
return fmt.Errorf("store: host %s: refusing to save a host whose sealed api_key did not open", h.HostID)
|
||||
@@ -3617,15 +3616,15 @@ func (s *Store) RotateHostAPIKey(hostID, newAPIKey string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveHostDRBundle stores the IDENTITY escrow blob + the NON-secret DR directive alongside the
|
||||
// existing K-escrow blob (slice 10D.1). The K-escrow row must already exist (slice-7 escrow upload);
|
||||
// this updates the additive 10D columns. The hub holds only ciphertext + non-secret directive.
|
||||
func (s *Store) SaveHostDRBundle(hostID string, identityBlob []byte, directiveJSON string) error {
|
||||
if directiveJSON == "" {
|
||||
directiveJSON = "{}"
|
||||
}
|
||||
res, err := s.db.Exec(`UPDATE host_escrow SET identity_blob = ?, directive_json = ?, updated_at = datetime('now') WHERE host_id = ?`,
|
||||
identityBlob, directiveJSON, hostID)
|
||||
// SaveHostDRBundle stores the IDENTITY escrow blob alongside the existing K-escrow blob (slice 10D.1). The K-escrow
|
||||
// row must already exist (slice-7 escrow upload). The hub holds only ciphertext.
|
||||
//
|
||||
// R-105 (`09` §3 decision 169): the non-secret "DR directive" that used to ride here is RETIRED — nothing produced it
|
||||
// but a by-hand selftest flag, and nothing read it; the recovery path reads the DR recipe, tenantsync and this blob.
|
||||
// The column stays in the schema and is never written. Pinned by TestR105_IdentitySaveLeavesDirectiveColumnAlone.
|
||||
func (s *Store) SaveHostDRBundle(hostID string, identityBlob []byte) error {
|
||||
res, err := s.db.Exec(`UPDATE host_escrow SET identity_blob = ?, updated_at = datetime('now') WHERE host_id = ?`,
|
||||
identityBlob, hostID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -3635,30 +3634,25 @@ func (s *Store) SaveHostDRBundle(hostID string, identityBlob []byte, directiveJS
|
||||
return nil
|
||||
}
|
||||
|
||||
// HostDRBundle is the full DR directive served to a re-enrolling box (slice 10D): the two OPAQUE
|
||||
// escrow blobs (K + identity — useless without R) + the non-secret directive fields.
|
||||
// HostDRBundle is what a re-enrolling box is served (slice 10D): the two OPAQUE escrow blobs (K + identity — useless
|
||||
// without R).
|
||||
type HostDRBundle struct {
|
||||
KEscrowBlob []byte
|
||||
IdentityBlob []byte
|
||||
DirectiveJSON string
|
||||
KEscrowBlob []byte
|
||||
IdentityBlob []byte
|
||||
}
|
||||
|
||||
// GetHostDRBundle returns a host's DR bundle (nil if no escrow row). The blobs are opaque — the hub
|
||||
// cannot open them (it has no R).
|
||||
func (s *Store) GetHostDRBundle(hostID string) (*HostDRBundle, error) {
|
||||
var b HostDRBundle
|
||||
var directive sql.NullString
|
||||
err := s.db.QueryRow(`SELECT blob, identity_blob, directive_json FROM host_escrow WHERE host_id = ?`, hostID).
|
||||
Scan(&b.KEscrowBlob, &b.IdentityBlob, &directive)
|
||||
err := s.db.QueryRow(`SELECT blob, identity_blob FROM host_escrow WHERE host_id = ?`, hostID).
|
||||
Scan(&b.KEscrowBlob, &b.IdentityBlob)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if directive.Valid {
|
||||
b.DirectiveJSON = directive.String
|
||||
}
|
||||
return &b, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user