bastion 0.53.2
ci / personal-data (push) Successful in 5s
ci / test (push) Successful in 3m4s
ci / fuzz (push) Successful in 3m45s

This commit is contained in:
Privatae
2026-10-20 04:42:14 +00:00
commit e4c9b69b46
471 changed files with 159794 additions and 0 deletions
+381
View File
@@ -0,0 +1,381 @@
//go:build !windows
// Tagged !windows. These tests were ALREADY unix-only — they use
// helpers that live behind `//go:build !windows` (sessionShellsPath,
// withConfigDir, withMarkerAt, setupBox) — so this file has never compiled for
// Windows and `GOOS=windows go vet ./...` has been red on it since it was
// written. The tag removes no coverage; it states a dependency that was already
// there so vet can run clean on that target, which is what makes a REAL Windows
// regression visible instead of being lost in a failure everyone scrolls past.
// Windows-safe coverage of the machine-authority gate lives in privilege_test.go.
package main
// One test per blocking finding from the anchoring review, plus the notes.
import (
"bufio"
"context"
"crypto/ed25519"
"encoding/base64"
"encoding/json"
"fmt"
"net"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/gorilla/websocket"
)
func grantMember(t *testing.T, caps []string) {
t.Helper()
priv := setupBox(t)
writePairingState(t, tMember, pairingPending)
future := time.Now().Add(time.Hour).UTC().Format(time.RFC3339)
terms, sig := signTerms(t, priv, grantTerms(tMember, caps, future))
if _, errOut, code := driveOp(t, tAnchor, "pairing_grant_sync", []string{terms, sig}); code != 0 {
t.Fatalf("grant: %d %s", code, errOut)
}
}
// The harness LANE is gated by caps, not only the mgmt op table. A member
// without "harness" gets a refused J frame + exit 126 before any job runs; the
// anchor and a member holding the cap reach the job (echo mode proves it ran).
func TestHarnessLaneGatedByCaps(t *testing.T) {
grantMember(t, []string{"observe"})
drive := func(self string) (string, int) {
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()
handleHarnessConn(ws, self, "")
}))
defer srv.Close()
c, _, err := websocket.DefaultDialer.Dial("ws"+strings.TrimPrefix(srv.URL, "http"), nil)
if err != nil {
t.Fatal(err)
}
defer c.Close()
// On an anchored box the run spends the person's approval (writes.go);
// testApproval signs one from a test device, "" on a local box.
hs := harnessSpec{HarnessID: "h1", Mode: "echo", Prompt: "ping"}
hs.Approval = testApproval(t, "harness:run", harnessArgs(hs))
spec, _ := json.Marshal(hs)
_ = c.WriteMessage(websocket.BinaryMessage, append([]byte("H"), spec...))
var out string
for {
_, data, err := c.ReadMessage()
if err != nil {
t.Fatal(err)
}
switch data[0] {
case 'J':
out = string(data[1:])
case 'X':
var code int
fmt.Sscanf(string(data[1:]), "%d", &code)
return out, code
}
}
}
if out, code := drive(tMember); code != 126 || !contains(out, "caps") {
t.Fatalf("a member without the harness cap must be refused on the lane: %d %s", code, out)
}
if out, code := drive(tAnchor); code != 0 || !contains(out, `"echo":"ping"`) {
t.Fatalf("the anchor's harness lane must run the job: %d %s", code, out)
}
// a member WITH the cap runs the job
writePairingState(t, tMember, pairingPending)
priv := setupBoxKeep(t)
future := time.Now().Add(time.Hour).UTC().Format(time.RFC3339)
terms, sig := signTerms(t, priv, grantTerms(tMember, []string{"harness"}, future))
if _, errOut, code := driveOp(t, tAnchor, "pairing_grant_sync", []string{terms, sig}); code != 0 {
t.Fatalf("re-grant: %d %s", code, errOut)
}
if out, code := drive(tMember); code != 0 || !contains(out, `"echo":"ping"`) {
t.Fatalf("a member with the harness cap must run the job: %d %s", code, out)
}
}
// setupBoxKeep re-pins a fresh anchor signing key on an already-set-up box
// (the previous key is not kept by setupBox), by rewriting the pin directly.
func setupBoxKeep(t *testing.T) ed25519.PrivateKey {
t.Helper()
f, err := loadAnchorFile()
if err != nil {
t.Fatal(err)
}
pub, priv, _ := ed25519.GenerateKey(nil)
f.VerifyPubkey = base64.StdEncoding.EncodeToString(pub)
if err := saveAnchorFile(f); err != nil {
t.Fatal(err)
}
return priv
}
// The egress LANE is gated too: a member without "egress" gets a 403
// before any dial; the anchor's request is dialled.
func TestEgressLaneGatedByCaps(t *testing.T) {
grantMember(t, []string{"observe"})
dialed := false
dialOut := func(ctx context.Context, network, addr string) (net.Conn, error) {
dialed = true
return nil, fmt.Errorf("stop here")
}
drive := func(self string) string {
srvSide, cliSide := net.Pipe()
go handleEgressConn(srvSide, dialOut, self)
_ = cliSide.SetDeadline(time.Now().Add(5 * time.Second))
fmt.Fprintf(cliSide, "CONNECT example.com:443 HTTP/1.1\r\nHost: example.com\r\n\r\n")
line, _ := bufio.NewReader(cliSide).ReadString('\n')
cliSide.Close()
return line
}
if line := drive(tMember); !contains(line, "403") || dialed {
t.Fatalf("a member without the egress cap must get 403 and no dial: %q dialed=%v", line, dialed)
}
if line := drive(tAnchor); !dialed || contains(line, "403") {
t.Fatalf("the anchor's egress must be dialled: %q dialed=%v", line, dialed)
}
}
// A runtime save (endpoint rotation directive) after a drop must leave
// State=revoked on disk. The heartbeat goroutine's Config was loaded while the
// pairing was active; the rotate path may only touch the endpoint fields.
func TestRotateDirectiveAfterDropKeepsRevoked(t *testing.T) {
setupBox(t)
writePairingState(t, tMember, pairingActive)
cfgs, _ := loadPairings()
running := pairingFor(cfgs, tMember) // the daemon's stale copy: State active
running.Endpoint, running.EndpointPubKey = "relay-a:51820", "pk"
// another writer revokes it on disk
disk := pairingFor(cfgs, tMember)
*disk = *running
disk.State = pairingRevoked
if err := disk.save(); err != nil {
t.Fatal(err)
}
// the host's rotate directive names the entry we are already on, with a
// new candidate list: the "keep the list current" save path
resp := []byte(`{"rotate_endpoint":{"endpoint":"relay-a:51820","endpoints":["relay-a:51820","relay-b:51820"],"endpoint_pubkey":"pk"}}`)
maybeRotateEndpoint(resp, running, nil)
if loadState(t, tMember) != pairingRevoked {
t.Fatal("a rotate save clobbered State back to active")
}
after, _ := loadPairings()
if got := pairingFor(after, tMember); len(got.Endpoints) != 2 {
t.Fatalf("the endpoint list must still have been saved: %v", got.Endpoints)
}
if running.State != pairingRevoked {
t.Fatal("updateOnDisk must refresh the in-memory State from disk")
}
}
// The daemon follows the files: a running pairing whose file flips to
// revoked (or disappears) is stopped on the next supervise pass.
func TestSuperviseFollowsDiskState(t *testing.T) {
setupBox(t)
writePairingState(t, tMember, pairingActive)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
done := make(chan struct{})
go func() { <-ctx.Done(); close(done) }()
supervisor.Lock()
supervisor.runs = map[string]*pairingRun{tMember: {cancel: cancel, done: done}}
supervisor.Unlock()
t.Cleanup(func() {
supervisor.Lock()
supervisor.runs = nil
supervisor.Unlock()
})
superviseOnce()
if ctx.Err() != nil {
t.Fatal("an active pairing must not be stopped")
}
writePairingState(t, tMember, pairingPending)
superviseOnce()
if ctx.Err() == nil {
t.Fatal("a pairing flipped to pending on disk must be stopped")
}
}
// A revoked host presenting a fresh token becomes PENDING (a new request
// for the anchor), and unpair removes the files.
func TestReplaceRevokedBecomesPendingAndUnpair(t *testing.T) {
setupBox(t)
writePairingState(t, tMember, pairingRevoked)
cfgs, _ := loadPairings()
_, st, err := pairPrecheck(cfgs, "member", true, false)
if err != nil || st != pairingPending {
t.Fatalf("--replace on a revoked pairing must write pending: %q %v", st, err)
}
// unpair: networked mode needs root — asserted through the seam so it holds
// whatever euid the suite runs under
if err := savePairingGrant(tMember, &pairingGrantFile{TermsCanonical: "x", Signature: "y"}); err != nil {
t.Fatal(err)
}
prevSeam := unpairNeedsRoot
t.Cleanup(func() { unpairNeedsRoot = prevSeam })
unpairNeedsRoot = func() bool { return true }
if _, err := unpairHost(tMember); err == nil || !contains(err.Error(), "root") {
t.Fatalf("unpair on a networked box must need root: %v", err)
}
// the real predicate: networked + non-root is exactly the refusing case
if got := prevSeam(); got != (os.Geteuid() != 0) {
t.Fatalf("unpairNeedsRoot on a networked box must be (euid != 0), got %v", got)
}
unpairNeedsRoot = func() bool { return false }
// the anchor is refused with the anchor present
if _, err := unpairHost(tAnchor); err == nil || !contains(err.Error(), "anchor") {
t.Fatalf("the anchor must not be unpairable: %v", err)
}
if currentAnchor() == nil {
t.Fatal("a refused unpair must not have touched the anchor")
}
host, err := unpairHost("member")
if err != nil || host != tMember {
t.Fatalf("unpair: %v", err)
}
pd, _ := pairingsDir()
for _, f := range []string{tMember + ".json", tMember + pairingGrantSuffix} {
if _, err := os.Stat(filepath.Join(pd, f)); err == nil {
t.Fatalf("unpair must delete %s", f)
}
}
if _, err := unpairHost("member"); err == nil {
t.Fatal("unpairing an unknown host must fail")
}
}
// A runtime save must never recreate an unpaired host's file. Delete the
// file under a running pairing's copy; saveEndpoints must fail and leave the
// file absent (a rotate/failover save inside the 15 s supervise window used to
// bring the host back with State active).
func TestRuntimeSaveNeverRecreatesAPairingFile(t *testing.T) {
setupBox(t)
writePairingState(t, tMember, pairingActive)
cfgs, _ := loadPairings()
running := pairingFor(cfgs, tMember)
pd, _ := pairingsDir()
p := filepath.Join(pd, tMember+".json")
if err := os.Remove(p); err != nil {
t.Fatal(err)
}
running.Endpoints = []string{"relay-a:51820"}
if err := running.saveEndpoints(); err != errPairingGone {
t.Fatalf("saveEndpoints on a gone file must return errPairingGone, got %v", err)
}
if _, err := os.Stat(p); err == nil {
t.Fatal("a runtime save recreated an unpaired host's file")
}
// the same through the handler path
if _, err := updatePairingOnDisk(tMember, func(d *Config) { d.State = pairingActive }); err != errPairingGone {
t.Fatalf("updatePairingOnDisk on a gone file must refuse: %v", err)
}
if _, err := os.Stat(p); err == nil {
t.Fatal("updatePairingOnDisk recreated the file")
}
}
// A pairing write goes through atomicWrite: no temp file is left behind
// and the file is whole; the lock file exists after any locked write.
func TestPairingSaveIsAtomicAndLocked(t *testing.T) {
setupBox(t)
c := &Config{HostName: tMember, State: pairingPending}
if err := c.save(); err != nil {
t.Fatal(err)
}
pd, _ := pairingsDir()
ents, _ := os.ReadDir(pd)
for _, e := range ents {
if strings.HasSuffix(e.Name(), ".tmp") {
t.Fatalf("temp file left behind: %s", e.Name())
}
}
cd, _ := configDir()
if _, err := os.Stat(filepath.Join(cd, "pairings.lock")); err != nil {
t.Fatal("a locked write must have created pairings.lock")
}
if loadState(t, tMember) != pairingPending {
t.Fatal("the written file must read back whole")
}
}
// On an anchored box an empty lane identity is default-deny; on an
// unanchored box nothing is gated.
func TestCapsAllowEmptySelfIsDefaultDenyWhenAnchored(t *testing.T) {
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
if why := capsAllow("", "egress"); why != "" {
t.Fatalf("an unanchored box gates nothing: %s", why)
}
setupBox(t)
if why := capsAllow("", "egress"); why == "" {
t.Fatal("an empty self on an anchored box must be refused")
}
}
// Notes — the two claims share a key and must not share a byte space; names
// with a newline are refused everywhere a claim is written.
func TestSigningInputsAreDomainSeparatedAndNewlinesRefused(t *testing.T) {
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
if string(ownerSigningInput("x\nhost", "t")) == string(anchorSigningInput("x", "host", "t")) {
t.Fatal("owner and anchor signing inputs collide")
}
if !strings.HasPrefix(string(ownerSigningInput("a", "t")), "owner\n") ||
!strings.HasPrefix(string(anchorSigningInput("a", "h", "t")), "anchor\n") {
t.Fatal("signing inputs must carry a kind tag")
}
for _, bad := range []string{"", "a\nb", "a\rb", strings.Repeat("x", 121)} {
if _, err := writeOwnerClaim(bad); err == nil {
t.Errorf("owner name %q must be refused", bad)
}
if err := writeAnchor(bad, tAnchor); err == nil {
t.Errorf("anchor owner %q must be refused", bad)
}
}
if err := writeAnchor("ok", "not/a/host"); err == nil {
t.Error("an invalid anchor host must be refused")
}
}
// Notes — peers carry their state so a pending host never shows as an
// established peer; since is first_seen and survives --replace.
func TestPeersCarryStateAndFirstSeen(t *testing.T) {
setupBox(t)
writePairingState(t, tMember, pairingPending)
peers := peersFor(tAnchor)
if len(peers) != 1 || peers[0].State != pairingPending {
t.Fatalf("a pending peer must announce its state: %+v", peers)
}
raw, _ := json.Marshal(peers)
if !contains(string(raw), `"state":"pending"`) {
t.Fatalf("state must be on the wire: %s", raw)
}
// first_seen wins over mtime and is kept by --replace
cfgs, _ := loadPairings()
m := pairingFor(cfgs, tMember)
m.FirstSeen = "2026-01-01T00:00:00Z"
_ = m.save()
if got := sinceOf(m); got != "2026-01-01T00:00:00Z" {
t.Fatalf("since must be first_seen: %s", got)
}
if e := pairingsReport(); e[1].Since != "2026-01-01T00:00:00Z" {
t.Fatalf("pairings report must use first_seen: %+v", e)
}
cfgs, _ = loadPairings()
existing, _, _ := pairPrecheck(cfgs, "member", true, false)
if existing.FirstSeen != "2026-01-01T00:00:00Z" {
t.Fatal("the existing pairing handed to --replace must carry first_seen for pairWithToken to keep")
}
}