//go:build !windows package main // hostops_test.go — the write gate on an anchored box (writes.go, device.go, // quarantine.go): every change needs an approval this box verified, reads // never do, a local box is untouched, and the mutations the design names — // unsigned / expired / uses-exhausted / wrong-key / no-nonce / replayed / // wrong-purpose approvals — are each refused. import ( "context" "crypto/ed25519" "encoding/base64" "encoding/json" "fmt" "net/http" "net/http/httptest" "os" "strings" "testing" "time" "github.com/gorilla/websocket" ) // testApproval signs a device approval for one run of a write on an anchored // test box, from a device key it creates and pins in the test's config dir. // "" when the gate does not apply (local box, a read, an op with its own // approval), so drivers can pass it unconditionally. func testApproval(t *testing.T, op string, args []string) string { t.Helper() if currentAnchor() == nil || !opWrites(op) || ownApproval(op) { return "" } pub, err := deviceInit() if err != nil { t.Fatalf("device-init: %v", err) } if err := pinDeviceKey(pub); err != nil { t.Fatalf("pin-device: %v", err) } hn, _ := os.Hostname() a, err := deviceSign(mgmtOpPurpose(op, hn, args), time.Minute) if err != nil { t.Fatalf("device-sign: %v", err) } return a } // testDirective attaches the person's approval for one heartbeat directive. // `key` is the approvals map key ("self-update", "contain:", "mirror"); // the purpose is bound to the directive name before any ":". func testDirective(t *testing.T, key string, args []string, body string) []byte { t.Helper() var m map[string]any if err := json.Unmarshal([]byte(body), &m); err != nil { t.Fatal(err) } what, _, _ := strings.Cut(key, ":") if a := testApproval(t, what, args); a != "" { m["approvals"] = map[string]string{key: a} } out, _ := json.Marshal(m) return out } // driveJob sends a job AS IS — no approval is added — with a configured // context root, so the reply is the gate's and not "context store not // configured". func driveJob(t *testing.T, self string, job mgmtJob, ctxRoot, dcnDir string) (string, string, int) { t.Helper() up := websocket.Upgrader{} srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ws, err := up.Upgrade(w, r, nil) if err != nil { return } defer ws.Close() handleMgmtConnFor(ws, self, ctxRoot, dcnDir, "") })) defer srv.Close() c, _, err := websocket.DefaultDialer.Dial("ws"+strings.TrimPrefix(srv.URL, "http"), nil) if err != nil { t.Fatalf("dial: %v", err) } defer c.Close() raw, _ := json.Marshal(job) if err := c.WriteMessage(websocket.BinaryMessage, append([]byte("C"), raw...)); err != nil { t.Fatalf("write: %v", err) } var out, errOut string for { _, data, err := c.ReadMessage() if err != nil { t.Fatalf("read: %v", err) } switch data[0] { case 'O': out += string(data[1:]) case 'R': errOut += string(data[1:]) case 'X': var code int fmt.Sscanf(string(data[1:]), "%d", &code) return out, errOut, code } } } // putJob is the gated write the gate tests drive: ctx_extract with an // argument its handler will refuse (an unknown tool), so a run that passes // the gate answers with the HANDLER's exit (1) — the gate's refusals are 126. // (ctx_put is standing work under the configured store and is not gated.) func putJob(approval string) mgmtJob { return mgmtJob{Op: "ctx_extract", Args: []string{`{"tool":"no-such-tool","mime":"text/plain","b64":"aGk="}`}, Approval: approval} } const putOp = "ctx_extract" // Every op this box answers is a read or a write, and the read set is exactly // the one the design names. A new op lands as a write until someone says // otherwise. func TestEveryOpIsReadOrWrite(t *testing.T) { reads := map[string]bool{} for _, op := range []string{"uptime", mgmtAuditFetchOp, "dcn_list", "dcn_stat", "dcn_read_range", "dcn_search", "dcn_catalog", "ctx_get", "backup_list", "model:list", "pairing_list", "stepup_request", "stepup_status", "device_request", "device_status", "op_list", "op_run_status", "op_plan_status"} { reads[op] = true } for _, op := range mgmtOpVocabulary() { isRead := reads[op] || strings.HasPrefix(op, "observe:") || strings.HasPrefix(op, "computer:") if opWrites(op) == isRead { t.Errorf("%s: opWrites=%v, the design says read=%v", op, opWrites(op), isRead) } } for _, op := range []string{"harness:run", "session:open", "contain", "self-update", "mirror", "backup_fetch", "op_sync", "op_dry_run", "op_apply", "op_grant_run", "brand_new_op"} { if !opWrites(op) { t.Errorf("%s must be a write", op) } } // egress and inference USE the machine and change nothing on it. for _, op := range []string{"egress", "inference"} { if opWrites(op) { t.Errorf("%s uses this machine and changes nothing on it — not gated per run", op) } } for _, op := range []string{"dcn_grant_drop", "pairing_grant_drop"} { if !opTightens(op) || !ownApproval(op) { t.Errorf("%s only removes reach and is never held back", op) } } for _, op := range []string{"dcn_grant_sync", "dcn_plan_apply", "model:pull", "pairing_grant_sync", "backup_fetch", "ctx_put", "ctx_del"} { if !ownApproval(op) { t.Errorf("%s carries its own approval and must not be gated twice", op) } } if ownApproval("ctx_extract") || ownApproval("model:drop") { t.Error("ctx_extract / model:drop carry no approval of their own") } for _, op := range []string{"backup_fetch", "ctx_put", "ctx_del"} { if !ownApproval(op) { t.Errorf("%s is standing work under a store the person configured at the machine", op) } } } // A local box gates nothing: the pairing is the authorization there. func TestALocalBoxRunsAWriteOnThePairingAlone(t *testing.T) { t.Setenv("XDG_CONFIG_HOME", t.TempDir()) writePairing(t, tAnchor) if _, errOut, code := driveJob(t, tAnchor, putJob(""), t.TempDir(), ""); code == 126 { t.Fatalf("a local box must run a write with no approval (the handler answers, not the gate): %d %s", code, errOut) } } // On an anchored box: a read answers; a write with no approval is refused and // names the fix; a write with a device approval runs exactly once. func TestAnAnchoredBoxRefusesAWriteWithoutApprovalAndRunsItOnceWithOne(t *testing.T) { setupBox(t) ctx := t.TempDir() if _, errOut, code := driveJob(t, tAnchor, mgmtJob{Op: "pairing_list"}, ctx, ""); code != 0 { t.Fatalf("a read must answer on the anchor's word: %d %s", code, errOut) } _, errOut, code := driveJob(t, tAnchor, putJob(""), ctx, "") if code != 126 || !strings.Contains(errOut, "holds no way to take one") || !strings.Contains(errOut, "pin-device") { t.Fatalf("no factor, no device: the refusal must name the fix: %d %q", code, errOut) } if !quarantinedUntil(tAnchor, time.Now()).IsZero() { t.Fatal("'no way to approve' is the box's own state and must not count as a strike") } if refusalCounts(putOp, "refused: op 'zzz' not permitted on this host") || refusalCounts("uptime", "refused: x") { t.Fatal("an unknown op or a read never counts") } if !refusalCounts(putOp, "refused: op 'ctx_extract' changes this machine and approval expired") { t.Fatal("a bad approval on a known write counts") } // Device pinned, no approval given. pub, _ := deviceInit() if err := pinDeviceKey(pub); err != nil { t.Fatal(err) } if !devicePinned() || !secondFactorAvailable("") { t.Fatal("the pin must count as a second factor") } _, errOut, code = driveJob(t, tAnchor, putJob(""), ctx, "") if code != 126 || !strings.Contains(errOut, "none was given") { t.Fatalf("a write with no approval is refused: %d %q", code, errOut) } // Approved: runs once. hn, _ := os.Hostname() approval, err := deviceSign(mgmtOpPurpose(putOp, hn, putJob("").Args), time.Minute) if err != nil { t.Fatal(err) } if _, errOut, code := driveJob(t, tAnchor, putJob(approval), ctx, ""); code == 126 { t.Fatalf("an approved write must reach the handler: %d %s", code, errOut) } // Replay: the nonce is burned. if _, errOut, code := driveJob(t, tAnchor, putJob(approval), ctx, ""); code != 126 || !strings.Contains(errOut, "already used") { t.Fatalf("a replayed approval must be refused: %d %q", code, errOut) } // The audit line names how it was approved and binds the args. recs := readMgmtAudit(t) var ran map[string]any for _, r := range recs { if r["verdict"] == auditAllowed && r["op"] == putOp { ran = r } } if ran == nil || !strings.HasPrefix(fmt.Sprint(ran["approval"]), "device:") || ran["args_sha256"] != argsDigest(putJob("").Args) { t.Fatalf("the allowed line must name the approval spent and the args digest: %v", ran) } } // The mutation suite: each broken approval is refused for its own reason. func TestEveryBrokenDeviceApprovalIsRefused(t *testing.T) { setupBox(t) ctx := t.TempDir() pub, _ := deviceInit() if err := pinDeviceKey(pub); err != nil { t.Fatal(err) } hn, _ := os.Hostname() purpose := mgmtOpPurpose(putOp, hn, putJob("").Args) good, _ := deviceSign(purpose, time.Minute) var a deviceApproval _ = json.Unmarshal([]byte(good), &a) mutate := func(f func(x *deviceApproval)) string { x := a f(&x) raw, _ := json.Marshal(&x) return string(raw) } cases := []struct{ name, approval, want string }{ {"wrong purpose (other args)", mutate(func(x *deviceApproval) { x.Purpose = mgmtOpPurpose(putOp, hn, []string{"other"}) }), "signature does not verify"}, {"tampered signature", mutate(func(x *deviceApproval) { x.Sig = base64.StdEncoding.EncodeToString(make([]byte, 64)) }), "signature does not verify"}, {"expired", mutate(func(x *deviceApproval) { x.Expires = time.Now().Add(-time.Minute).UTC().Format(time.RFC3339) }), "signature does not verify"}, {"two uses", mutate(func(x *deviceApproval) { x.Uses = 2 }), "signature does not verify"}, {"no nonce", mutate(func(x *deviceApproval) { x.Nonce = "" }), "signature does not verify"}, {"other version", mutate(func(x *deviceApproval) { x.V = 2 }), "not one this box reads"}, {"unreadable", `{"sig": not json`, "not readable"}, {"not an approval at all", "{not json", "none was given"}, {"console id that does not exist", "deadbeef", "none was given"}, } // Every refused write here is a strike (quarantine.go); the hold is lifted // between cases so each case meets the gate and not the hold. for _, c := range cases { _, errOut, code := driveJob(t, tAnchor, putJob(c.approval), ctx, "") if code != 126 || !strings.Contains(errOut, c.want) { t.Errorf("%s: want refusal containing %q, got %d %q", c.name, c.want, code, errOut) } quarantineClear(tAnchor) } // Properly RE-SIGNED mutations: the signature is fine, the content is not. resign := func(f func(x *deviceApproval)) string { x := a f(&x) f2, _ := deviceKeyLoad() priv, _ := base64.StdEncoding.DecodeString(f2.Priv) x.Sig = base64.StdEncoding.EncodeToString(ed25519.Sign(ed25519.PrivateKey(priv), x.canonical())) raw, _ := json.Marshal(&x) return string(raw) } resigned := []struct{ name, approval, want string }{ {"wrong purpose", resign(func(x *deviceApproval) { x.Purpose = mgmtOpPurpose(putOp, hn, []string{"other"}) }), "purpose mismatch"}, {"expired", resign(func(x *deviceApproval) { x.Expires = time.Now().Add(-time.Minute).UTC().Format(time.RFC3339) }), "expired"}, {"too long-lived", resign(func(x *deviceApproval) { x.Expires = time.Now().Add(48 * time.Hour).UTC().Format(time.RFC3339) }), "longer than"}, {"two uses", resign(func(x *deviceApproval) { x.Uses = 2 }), "one run"}, {"short nonce", resign(func(x *deviceApproval) { x.Nonce = "abc" }), "nonce too short"}, } for _, c := range resigned { _, errOut, code := driveJob(t, tAnchor, putJob(c.approval), ctx, "") if code != 126 || !strings.Contains(errOut, c.want) { t.Errorf("%s: want refusal containing %q, got %d %q", c.name, c.want, code, errOut) } quarantineClear(tAnchor) } // And the good one still runs after all of that: no mutation spent it. if _, errOut, code := driveJob(t, tAnchor, putJob(good), ctx, ""); code == 126 { t.Fatalf("the untouched approval must still run: %d %s", code, errOut) } } // A device the box never pinned signs nothing that runs here, and unpinning // withdraws a device. func TestAnUnpinnedDeviceIsRefusedAndUnpinWithdrawsOne(t *testing.T) { // Device A on its own machine. t.Setenv("XDG_CONFIG_HOME", t.TempDir()) pubA, _ := deviceInit() hn, _ := os.Hostname() fromA, _ := deviceSign(mgmtOpPurpose(putOp, hn, putJob("").Args), time.Minute) // The anchored box, with device B pinned. setupBox(t) ctx := t.TempDir() pubB, _ := deviceInit() if err := pinDeviceKey(pubB); err != nil { t.Fatal(err) } if _, errOut, code := driveJob(t, tAnchor, putJob(fromA), ctx, ""); code != 126 || !strings.Contains(errOut, "not pinned") { t.Fatalf("an unpinned device's approval must be refused: %d %q", code, errOut) } if err := pinDeviceKey(pubA); err != nil { t.Fatal(err) } if _, errOut, code := driveJob(t, tAnchor, putJob(fromA), ctx, ""); code == 126 { t.Fatalf("once pinned, A's approval runs: %d %s", code, errOut) } if err := unpinDeviceKey(pubA); err != nil { t.Fatal(err) } if err := unpinDeviceKey(pubB); err != nil { t.Fatal(err) } if devicePinned() { t.Fatal("no device is pinned now") } if err := unpinDeviceKey(pubB); err == nil { t.Fatal("unpinning a key that is not pinned must say so") } if err := pinDeviceKey("not-a-key"); err == nil { t.Fatal("a malformed key must be refused") } } // The console path: a factor enrolled here, a mgmt_op request the host files, // the person approves with their code, and the approval runs the op once. func TestAConsoleApprovalRunsAWriteOnce(t *testing.T) { setupBox(t) ctx, dcn := t.TempDir(), t.TempDir() enrollFactor(t, dcn) hn, _ := os.Hostname() args := putJob("").Args details := map[string]any{"kind": "mgmt_op", "op": putOp, "server": hn, "args": []any{args[0]}, "note": "save the meeting notes"} purpose, err := purposeFromDetails(details) if err != nil { t.Fatal(err) } if purpose != mgmtOpPurpose(putOp, hn, args) { t.Fatalf("the purpose must be derived from the details: %s", purpose) } if stepupDetailsFamily(details) != "dcn" { t.Fatalf("a mgmt_op ask is gated on the op's family: %s", stepupDetailsFamily(details)) } if stepupDetailsFamily(map[string]any{"kind": "mgmt_op", "op": "contain", "server": hn}) != capsFamilyAnchor { t.Fatal("a directive's approval is the anchor's alone") } elsewhere := map[string]any{"kind": "mgmt_op", "op": "ctx_put", "server": "elsewhere", "args": []any{"a"}} ep, _ := purposeFromDetails(elsewhere) if _, err := stepupNewRequest(dcn, ep, elsewhere); err == nil || !strings.Contains(err.Error(), "elsewhere") { t.Fatalf("a request for another machine must be refused at THIS box's ask: %v", err) } // The host files the ask on the mgmt lane. detailsRaw, _ := json.Marshal(details) detailsB64 := base64.StdEncoding.EncodeToString(detailsRaw) out, errOut, code := driveJob(t, tAnchor, mgmtJob{Op: "stepup_request", Args: []string{purpose, detailsB64}}, ctx, dcn) if code != 0 { t.Fatalf("stepup_request: %d %s", code, errOut) } var reply struct{ Request string } _ = json.Unmarshal([]byte(out), &reply) if words := describeRequest(stepupRequest{Details: details}); !strings.Contains(words, putOp) || !strings.Contains(words, "on "+hn) || !strings.Contains(words, "arg 1: "+args[0]) || strings.Contains(words, "meeting notes") { t.Fatalf("the console shows the op, the machine and every argument — never the pod's note: %q", words) } if long := argWords(strings.Repeat("x", 3000)); !strings.Contains(long, "3000 bytes in all") { t.Fatalf("a long argument is named by its length and digest: %q", long[len(long)-60:]) } raw, _ := b32decode(tSecret) approval, err := stepupApproveRequest(dcn, reply.Request, hotp(raw, time.Now().Unix()/stepupPeriod)) if err != nil { t.Fatal(err) } if _, errOut, code := driveJob(t, tAnchor, putJob(approval), ctx, dcn); code == 126 { t.Fatalf("the console approval must run the op: %d %s", code, errOut) } if _, errOut, code := driveJob(t, tAnchor, putJob(approval), ctx, dcn); code != 126 || !strings.Contains(errOut, "already used") { t.Fatalf("single use: %d %q", code, errOut) } // The console path hands a missed tap back on a re-file, once. The first // request above was approved directly (never read through stepup_status), // so it is still undelivered: a re-file returns IT, approved. same, err := stepupNewRequest(dcn, purpose, details) if err != nil || same != reply.Request { t.Fatalf("an approved, undelivered ask is the same request: %s %v", same, err) } if _, ok := stepupHandOut(dcn, reply.Request); !ok { t.Fatal("handed out once") } id2, err := stepupNewRequest(dcn, purpose, details) if err != nil || id2 == reply.Request { t.Fatalf("after delivery the same ask is a new request: %s %v", id2, err) } if _, err := stepupApproveRequest(dcn, id2, hotp(raw, time.Now().Unix()/stepupPeriod+1)); err != nil { t.Fatal(err) } out2, _, code := driveJob(t, tAnchor, mgmtJob{Op: "stepup_request", Args: []string{purpose, detailsB64}}, ctx, dcn) if code != 0 || !strings.Contains(out2, `"approved"`) || !strings.Contains(out2, id2) { t.Fatalf("a re-filed approved ask is answered approved with its id: %d %s", code, out2) } st, _, _ := driveJob(t, tAnchor, mgmtJob{Op: "stepup_status", Args: []string{id2}}, ctx, dcn) if !strings.Contains(st, `"delivered"`) { t.Fatalf("handed out once: %s", st) } } // Five refused writes inside ten minutes close a pairing's write lanes; reads // stay open; the console lifts it. func TestRepeatedRefusedWritesQuarantineThePairing(t *testing.T) { setupBox(t) ctx := t.TempDir() // A device is pinned, so every refusal below is the host's fault (no // approval given), the kind that counts. pub, _ := deviceInit() _ = pinDeviceKey(pub) for i := 0; i < quarantineStrikes; i++ { if _, errOut, code := driveJob(t, tAnchor, putJob(""), ctx, ""); code != 126 || strings.Contains(errOut, "quarantined") { t.Fatalf("refusal %d must be the gate's, not a hold yet: %d %q", i+1, code, errOut) } } if _, errOut, code := driveJob(t, tAnchor, putJob(""), ctx, ""); code != 126 || !strings.Contains(errOut, "quarantined") { t.Fatalf("the sixth write must meet the hold: %d %q", code, errOut) } // Even a GOOD approval is refused during the hold, and not spent — and a // revoke is never held back. hn, _ := os.Hostname() if _, errOut, code := driveJob(t, tAnchor, mgmtJob{Op: "dcn_grant_drop", Args: []string{"7"}}, ctx, t.TempDir()); code == 126 && strings.Contains(errOut, "quarantined") { t.Fatalf("a tighten must pass a hold: %d %q", code, errOut) } approval, _ := deviceSign(mgmtOpPurpose(putOp, hn, putJob("").Args), time.Minute) if _, errOut, code := driveJob(t, tAnchor, putJob(approval), ctx, ""); code != 126 || !strings.Contains(errOut, "quarantined") { t.Fatalf("a held pairing's approved write is still refused: %d %q", code, errOut) } if _, errOut, code := driveJob(t, tAnchor, mgmtJob{Op: "pairing_list"}, ctx, ""); code != 0 { t.Fatalf("reads stay open under a hold: %d %s", code, errOut) } rep := buildEnforcingReport("", nil) if rep["write_gate"] != true || rep["device"] != true { t.Fatalf("the report must say the gate is on and a device is pinned: %v", rep) } held, _ := rep["quarantined"].([]string) if len(held) != 1 || held[0] != tAnchor { t.Fatalf("the report must name the held pairing: %v", rep["quarantined"]) } if ring, _ := rep["refusals"].([]refusalRecord); len(ring) < quarantineStrikes { t.Fatalf("the report must carry the refusal ring: %v", rep["refusals"]) } quarantineClear(tAnchor) if _, errOut, code := driveJob(t, tAnchor, putJob(approval), ctx, ""); code == 126 { t.Fatalf("after the console lifts the hold, the approval (never spent) runs: %d %s", code, errOut) } // A second hold doubles. for i := 0; i < quarantineStrikes; i++ { driveJob(t, tAnchor, putJob(""), ctx, "") } until := quarantinedUntil(tAnchor, time.Now()) if until.IsZero() { t.Fatal("second hold") } quarantineClear(tAnchor) // forgiven: strikes reset for i := 0; i < quarantineStrikes; i++ { driveJob(t, tAnchor, putJob(""), ctx, "") } if u := quarantinedUntil(tAnchor, time.Now()); u.IsZero() || time.Until(u) > quarantineBase+time.Minute { t.Fatalf("a cleared hold is forgiven — the next one is the base length again: %v", u) } // Strikes double when the hold lapses on its own. f := quarantineLoad() f.Pairing[tAnchor].Until = time.Now().Add(-time.Second).UTC().Format(time.RFC3339) _ = quarantineSave(f) for i := 0; i < quarantineStrikes; i++ { driveJob(t, tAnchor, putJob(""), ctx, "") } if u := quarantinedUntil(tAnchor, time.Now()); u.IsZero() || time.Until(u) < 2*quarantineBase-time.Minute { t.Fatalf("a repeat hold doubles: %v", u) } } // The person at the machine switches the directives off, and the anchor's // approved directive is refused anyway. // The args digest is what the pod derives too: a fixed vector, framed // ":\n" per argument, never JSON. func TestArgsDigestIsTheFramedSHA256(t *testing.T) { if got := argsDigest(nil); got != "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" { t.Fatalf("no args: %s", got) } // sha256 of "1:a\n3:b