bastion 0.53.2
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user