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
+151
View File
@@ -0,0 +1,151 @@
package main
// Gate: a restore request is bound to the pairing that sent it. A host that
// was taken over holds the bastion key's sidecars of every host that shares
// this bastion's key (a shared folder, or the platform), so the request's own
// words cannot decide whose backup it is: the host number this machine paired
// with does. Host A asking for host B's backup key is refused, whatever it
// calls itself, and nothing is sealed to A.
import (
"encoding/json"
"fmt"
"path/filepath"
"strings"
"testing"
"filippo.io/age"
)
// withFixturePairings pairs this machine (in a private config folder) with
// one host per name, host number n at fd00:9::e:<n hex>, as the platform
// hands the address out at pairing. Returns each pairing's DCN folder.
func withFixturePairings(t *testing.T, hosts map[string]int64) map[string]string {
t.Helper()
isolateConfigDir(t)
dirs := map[string]string{}
for name, n := range hosts {
dir := filepath.Join(t.TempDir(), "dcn")
c := &Config{HostName: name, HostTunnelIP: fmt.Sprintf("fd00:9::e:%x", n), DCNDir: dir,
BackupRoot: filepath.Join(t.TempDir(), "backups")}
if err := c.save(); err != nil {
t.Fatal(err)
}
dirs[name] = c.dcnDir()
}
return dirs
}
func resealStatusAs(t *testing.T, self, dcn, id string) map[string]string {
t.Helper()
out, errOut, code := driveBackupOpAs(t, self, "backup_reseal", dcn, "", resealReq{Op: "status", JobID: id})
if code != 0 {
t.Fatalf("status exited %d: %s", code, errOut)
}
var m map[string]string
if err := json.Unmarshal([]byte(out), &m); err != nil {
t.Fatalf("status is not JSON: %s", out)
}
return m
}
func resealStartAs(t *testing.T, self, dcn string, req resealReq) string {
t.Helper()
out, errOut, code := driveBackupOpAs(t, self, "backup_reseal", dcn, "", req)
if code != 0 {
t.Fatalf("start from %s exited %d: %s", self, code, errOut)
}
var m map[string]string
_ = json.Unmarshal([]byte(out), &m)
if m["state"] != "pending" || m["job_id"] == "" {
t.Fatalf("start from %s filed nothing: %s", self, out)
}
return m["job_id"]
}
// The attack: host A (number 7) was taken over. It holds host B's key file
// (B is number 42, the fixture's host), names itself "B", and asks for that
// backup's key sealed to its own key. The person approves at this machine.
// Nothing reaches A. B asking for its own backup still works.
func TestResealRefusesAnotherHostsBackupWhateverTheHostSays(t *testing.T) {
const hostA, hostB = "a7.host.privatae.ai", fixtureHost
dirs := withFixturePairings(t, map[string]int64{hostA: 7, hostB: fixtureEntity})
fx := newBackupFixture(t, "") // host fixtureEntity's (B's) backup
fx.installAsBastionKey(t)
attacker, _ := age.GenerateX25519Identity()
id := resealStartAs(t, hostA, dirs[hostA], resealReq{Op: "start", Sidecar: string(fx.sidecar(t)),
To: attacker.Recipient().String(), Stamp: fixtureStamp, HostLabel: hostB})
err := cmdBackupDecide(hostA, id, true)
if err == nil {
t.Fatal("the person's approval went through for another host's backup")
}
if !strings.Contains(err.Error(), "host number 42") || !strings.Contains(err.Error(), "host number 7") {
t.Errorf("the refusal does not say whose backup it is and who asked: %v", err)
}
st := resealStatusAs(t, hostA, dirs[hostA], id)
if st["state"] == "done" || st["sidecar"] != "" {
t.Fatalf("host A was handed host B's backup key: %v", st)
}
if st["state"] != "refused" {
t.Fatalf("the request reads %v, not refused", st)
}
// B, asking for its own backup, is served.
own, _ := age.GenerateX25519Identity()
idB := resealStartAs(t, hostB, dirs[hostB], resealReq{Op: "start", Sidecar: string(fx.sidecar(t)),
To: own.Recipient().String(), Stamp: fixtureStamp, HostLabel: "B"})
if err := cmdBackupDecide(hostB, idB, true); err != nil {
t.Fatalf("a host's own backup was refused: %v", err)
}
if st := resealStatusAs(t, hostB, dirs[hostB], idB); st["state"] != "done" || st["sidecar"] == "" {
t.Fatalf("host B did not get its own backup's key: %v", st)
}
}
// The refusal does not rest on the CLI's preview: resealDecide itself opens
// the sidecar and refuses another host's backup, and a request filed by one
// pairing cannot be approved as another's.
func TestResealDecideRefusesWhatIsNotThePairingsOwn(t *testing.T) {
fx := newBackupFixture(t, "")
fx.installAsBastionKey(t)
dir := t.TempDir()
host, _ := age.GenerateX25519Identity()
a := resealWho{Name: "a7.host.privatae.ai", Number: 7}
r, err := resealFile(dir, a, resealReq{Sidecar: string(fx.sidecar(t)), To: host.Recipient().String(), Stamp: fixtureStamp})
if err != nil {
t.Fatal(err)
}
if r.Pairing != a.Name || r.HostNumber != 7 {
t.Fatalf("the request is not bound to the pairing that filed it: %+v", r)
}
// Approved as if by host 42's pairing: the request is A's, refused.
got, err := resealDecide(dir, fixtureWho, r.ID, true)
if err != nil || got.State != "refused" || got.Resealed != "" || !strings.Contains(got.Error, "filed by") {
t.Fatalf("a request was approved for a pairing that did not file it: %+v %v", got, err)
}
r, _ = resealFile(dir, a, resealReq{Sidecar: string(fx.sidecar(t)), To: mustRecipient(t), Stamp: fixtureStamp})
got, err = resealDecide(dir, a, r.ID, true)
if err != nil || got.State != "refused" || got.Resealed != "" || !strings.Contains(got.Error, "host number 42") {
t.Fatalf("host 7 was given host 42's backup key: %+v %v", got, err)
}
if _, err := resealFile(dir, resealWho{Name: "x"}, resealReq{Sidecar: string(fx.sidecar(t)), To: mustRecipient(t),
Stamp: fixtureStamp}); err == nil {
t.Fatal("a request with no host number was filed")
}
}
func TestHostNumberComesFromTheTunnelAddress(t *testing.T) {
for ip, want := range map[string]int64{"fd00:9::e:2a": 42, "fd00:9::e:1": 1, "fd00:9::e:ffff": 0xffff,
"FD00:0009:0000:0000:0000:0000:000E:0007": 7} {
if n, ok := hostNumberOf(ip); !ok || n != want {
t.Errorf("%s: %d %v, want %d", ip, n, ok, want)
}
}
for _, ip := range []string{"", "fd00:9::e:0", "fd00:9::a:2a", "fd00:8::e:2a", "fd00:9:0:1::e:2a", "10.0.0.42",
"::ffff:10.0.0.42", "fd00:9::e:2a%eth0", "fd00:9::1:e:2a", "not an address"} {
if n, ok := hostNumberOf(ip); ok {
t.Errorf("%q read as host number %d", ip, n)
}
}
}