From ad7e389eee5f49c47576c69122af08fc9c51ebd7 Mon Sep 17 00:00:00 2001 From: Ophestra Umiker Date: Fri, 25 Oct 2024 17:12:13 +0900 Subject: [PATCH] app: test app permissive defaults sealing behaviour This test seals App against a deterministic os stub and checks the resulting sys and bwrap values against known correct ones. The effects of sys and bwrap on the OS and sandbox is deterministic and tested in their own respective packages. Signed-off-by: Ophestra Umiker --- internal/app/app_test.go | 379 ++++++++++++++++++++++++++++++++++++ internal/app/export_test.go | 19 ++ 2 files changed, 398 insertions(+) create mode 100644 internal/app/app_test.go create mode 100644 internal/app/export_test.go diff --git a/internal/app/app_test.go b/internal/app/app_test.go new file mode 100644 index 0000000..ed98a6b --- /dev/null +++ b/internal/app/app_test.go @@ -0,0 +1,379 @@ +package app_test + +import ( + "fmt" + "io/fs" + "os/user" + "reflect" + "strconv" + "testing" + + "git.ophivana.moe/security/fortify/acl" + "git.ophivana.moe/security/fortify/helper/bwrap" + "git.ophivana.moe/security/fortify/internal" + "git.ophivana.moe/security/fortify/internal/app" + "git.ophivana.moe/security/fortify/internal/system" +) + +func TestApp(t *testing.T) { + testCases := []struct { + name string + config *app.Config + id app.ID + wantSys *system.I + wantBwrap *bwrap.Config + }{ + { + "permissive defaults no enablements", + &app.Config{ + User: "chronos", + Command: make([]string, 0), + Method: "sudo", + }, + app.ID{ + 0x4a, 0x45, 0x0b, 0x65, + 0x96, 0xd7, 0xbc, 0x15, + 0xbd, 0x01, 0x78, 0x0e, + 0xb9, 0xa6, 0x07, 0xac, + }, + system.New(150). + Ensure("/tmp/fortify.1971", 0701). + Ephemeral(system.Process, "/tmp/fortify.1971/4a450b6596d7bc15bd01780eb9a607ac", 0701). + Ensure("/tmp/fortify.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir", acl.Execute). + Ensure("/tmp/fortify.1971/tmpdir/150", 01700).UpdatePermType(system.User, "/tmp/fortify.1971/tmpdir/150", acl.Read, acl.Write, acl.Execute). + Ensure("/run/user/1971/fortify", 0700).UpdatePermType(system.User, "/run/user/1971/fortify", acl.Execute). + Ensure("/run/user/1971", 0700).UpdatePermType(system.User, "/run/user/1971", acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset + Ephemeral(system.Process, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/4a450b6596d7bc15bd01780eb9a607ac", acl.Execute). + WriteType(system.Process, "/tmp/fortify.1971/4a450b6596d7bc15bd01780eb9a607ac/passwd", "chronos:x:65534:65534:Fortify:/home/chronos:/run/current-system/sw/bin/zsh\n"). + WriteType(system.Process, "/tmp/fortify.1971/4a450b6596d7bc15bd01780eb9a607ac/group", "fortify:x:65534:\n"), + (&bwrap.Config{ + Net: true, + UserNS: true, + Clearenv: true, + SetEnv: map[string]string{ + "HOME": "/home/chronos", + "SHELL": "/run/current-system/sw/bin/zsh", + "TERM": "xterm-256color", + "USER": "chronos", + "XDG_RUNTIME_DIR": "/run/user/150", + "XDG_SESSION_CLASS": "user", + "XDG_SESSION_TYPE": "tty"}, + Chmod: make(bwrap.ChmodConfig), + DieWithParent: true, + AsInit: true, + }).SetUID(65534).SetGID(65534). + Procfs("/proc").DevTmpfs("/dev").Mqueue("/dev/mqueue"). + Tmpfs("/dev/fortify", 4096). + Bind("/bin", "/bin", false, true). + Bind("/boot", "/boot", false, true). + Bind("/etc", "/dev/fortify/etc"). + Bind("/home", "/home", false, true). + Bind("/lib", "/lib", false, true). + Bind("/lib64", "/lib64", false, true). + Bind("/nix", "/nix", false, true). + Bind("/root", "/root", false, true). + Bind("/srv", "/srv", false, true). + Bind("/sys", "/sys", false, true). + Bind("/usr", "/usr", false, true). + Bind("/var", "/var", false, true). + Bind("/run/agetty.reload", "/run/agetty.reload", false, true). + Bind("/run/binfmt", "/run/binfmt", false, true). + Bind("/run/booted-system", "/run/booted-system", false, true). + Bind("/run/credentials", "/run/credentials", false, true). + Bind("/run/cryptsetup", "/run/cryptsetup", false, true). + Bind("/run/current-system", "/run/current-system", false, true). + Bind("/run/host", "/run/host", false, true). + Bind("/run/keys", "/run/keys", false, true). + Bind("/run/libvirt", "/run/libvirt", false, true). + Bind("/run/libvirtd.pid", "/run/libvirtd.pid", false, true). + Bind("/run/lock", "/run/lock", false, true). + Bind("/run/log", "/run/log", false, true). + Bind("/run/lvm", "/run/lvm", false, true). + Bind("/run/mount", "/run/mount", false, true). + Bind("/run/NetworkManager", "/run/NetworkManager", false, true). + Bind("/run/nginx", "/run/nginx", false, true). + Bind("/run/nixos", "/run/nixos", false, true). + Bind("/run/nscd", "/run/nscd", false, true). + Bind("/run/opengl-driver", "/run/opengl-driver", false, true). + Bind("/run/pppd", "/run/pppd", false, true). + Bind("/run/resolvconf", "/run/resolvconf", false, true). + Bind("/run/sddm", "/run/sddm", false, true). + Bind("/run/store", "/run/store", false, true). + Bind("/run/syncoid", "/run/syncoid", false, true). + Bind("/run/system", "/run/system", false, true). + Bind("/run/systemd", "/run/systemd", false, true). + Bind("/run/tmpfiles.d", "/run/tmpfiles.d", false, true). + Bind("/run/udev", "/run/udev", false, true). + Bind("/run/udisks2", "/run/udisks2", false, true). + Bind("/run/utmp", "/run/utmp", false, true). + Bind("/run/virtlogd.pid", "/run/virtlogd.pid", false, true). + Bind("/run/wrappers", "/run/wrappers", false, true). + Bind("/run/zed.pid", "/run/zed.pid", false, true). + Bind("/run/zed.state", "/run/zed.state", false, true). + Symlink("/dev/fortify/etc/alsa", "/etc/alsa"). + Symlink("/dev/fortify/etc/bashrc", "/etc/bashrc"). + Symlink("/dev/fortify/etc/binfmt.d", "/etc/binfmt.d"). + Symlink("/dev/fortify/etc/dbus-1", "/etc/dbus-1"). + Symlink("/dev/fortify/etc/default", "/etc/default"). + Symlink("/dev/fortify/etc/ethertypes", "/etc/ethertypes"). + Symlink("/dev/fortify/etc/fonts", "/etc/fonts"). + Symlink("/dev/fortify/etc/fstab", "/etc/fstab"). + Symlink("/dev/fortify/etc/fuse.conf", "/etc/fuse.conf"). + Symlink("/dev/fortify/etc/host.conf", "/etc/host.conf"). + Symlink("/dev/fortify/etc/hostid", "/etc/hostid"). + Symlink("/dev/fortify/etc/hostname", "/etc/hostname"). + Symlink("/dev/fortify/etc/hostname.CHECKSUM", "/etc/hostname.CHECKSUM"). + Symlink("/dev/fortify/etc/hosts", "/etc/hosts"). + Symlink("/dev/fortify/etc/inputrc", "/etc/inputrc"). + Symlink("/dev/fortify/etc/ipsec.d", "/etc/ipsec.d"). + Symlink("/dev/fortify/etc/issue", "/etc/issue"). + Symlink("/dev/fortify/etc/kbd", "/etc/kbd"). + Symlink("/dev/fortify/etc/libblockdev", "/etc/libblockdev"). + Symlink("/dev/fortify/etc/locale.conf", "/etc/locale.conf"). + Symlink("/dev/fortify/etc/localtime", "/etc/localtime"). + Symlink("/dev/fortify/etc/login.defs", "/etc/login.defs"). + Symlink("/dev/fortify/etc/lsb-release", "/etc/lsb-release"). + Symlink("/dev/fortify/etc/lvm", "/etc/lvm"). + Symlink("/dev/fortify/etc/machine-id", "/etc/machine-id"). + Symlink("/dev/fortify/etc/man_db.conf", "/etc/man_db.conf"). + Symlink("/dev/fortify/etc/modprobe.d", "/etc/modprobe.d"). + Symlink("/dev/fortify/etc/modules-load.d", "/etc/modules-load.d"). + Symlink("/proc/mounts", "/etc/mtab"). + Symlink("/dev/fortify/etc/nanorc", "/etc/nanorc"). + Symlink("/dev/fortify/etc/netgroup", "/etc/netgroup"). + Symlink("/dev/fortify/etc/NetworkManager", "/etc/NetworkManager"). + Symlink("/dev/fortify/etc/nix", "/etc/nix"). + Symlink("/dev/fortify/etc/nixos", "/etc/nixos"). + Symlink("/dev/fortify/etc/NIXOS", "/etc/NIXOS"). + Symlink("/dev/fortify/etc/nscd.conf", "/etc/nscd.conf"). + Symlink("/dev/fortify/etc/nsswitch.conf", "/etc/nsswitch.conf"). + Symlink("/dev/fortify/etc/opensnitchd", "/etc/opensnitchd"). + Symlink("/dev/fortify/etc/os-release", "/etc/os-release"). + Symlink("/dev/fortify/etc/pam", "/etc/pam"). + Symlink("/dev/fortify/etc/pam.d", "/etc/pam.d"). + Symlink("/dev/fortify/etc/pipewire", "/etc/pipewire"). + Symlink("/dev/fortify/etc/pki", "/etc/pki"). + Symlink("/dev/fortify/etc/polkit-1", "/etc/polkit-1"). + Symlink("/dev/fortify/etc/profile", "/etc/profile"). + Symlink("/dev/fortify/etc/protocols", "/etc/protocols"). + Symlink("/dev/fortify/etc/qemu", "/etc/qemu"). + Symlink("/dev/fortify/etc/resolv.conf", "/etc/resolv.conf"). + Symlink("/dev/fortify/etc/resolvconf.conf", "/etc/resolvconf.conf"). + Symlink("/dev/fortify/etc/rpc", "/etc/rpc"). + Symlink("/dev/fortify/etc/samba", "/etc/samba"). + Symlink("/dev/fortify/etc/sddm.conf", "/etc/sddm.conf"). + Symlink("/dev/fortify/etc/secureboot", "/etc/secureboot"). + Symlink("/dev/fortify/etc/services", "/etc/services"). + Symlink("/dev/fortify/etc/set-environment", "/etc/set-environment"). + Symlink("/dev/fortify/etc/shadow", "/etc/shadow"). + Symlink("/dev/fortify/etc/shells", "/etc/shells"). + Symlink("/dev/fortify/etc/ssh", "/etc/ssh"). + Symlink("/dev/fortify/etc/ssl", "/etc/ssl"). + Symlink("/dev/fortify/etc/static", "/etc/static"). + Symlink("/dev/fortify/etc/subgid", "/etc/subgid"). + Symlink("/dev/fortify/etc/subuid", "/etc/subuid"). + Symlink("/dev/fortify/etc/sudoers", "/etc/sudoers"). + Symlink("/dev/fortify/etc/sysctl.d", "/etc/sysctl.d"). + Symlink("/dev/fortify/etc/systemd", "/etc/systemd"). + Symlink("/dev/fortify/etc/terminfo", "/etc/terminfo"). + Symlink("/dev/fortify/etc/tmpfiles.d", "/etc/tmpfiles.d"). + Symlink("/dev/fortify/etc/udev", "/etc/udev"). + Symlink("/dev/fortify/etc/udisks2", "/etc/udisks2"). + Symlink("/dev/fortify/etc/UPower", "/etc/UPower"). + Symlink("/dev/fortify/etc/vconsole.conf", "/etc/vconsole.conf"). + Symlink("/dev/fortify/etc/X11", "/etc/X11"). + Symlink("/dev/fortify/etc/zfs", "/etc/zfs"). + Symlink("/dev/fortify/etc/zinputrc", "/etc/zinputrc"). + Symlink("/dev/fortify/etc/zoneinfo", "/etc/zoneinfo"). + Symlink("/dev/fortify/etc/zprofile", "/etc/zprofile"). + Symlink("/dev/fortify/etc/zshenv", "/etc/zshenv"). + Symlink("/dev/fortify/etc/zshrc", "/etc/zshrc"). + Bind("/tmp/fortify.1971/tmpdir/150", "/tmp", false, true). + Tmpfs("/tmp/fortify.1971", 1048576). + Tmpfs("/run/user", 1048576). + Tmpfs("/run/user/150", 8388608). + Bind("/tmp/fortify.1971/4a450b6596d7bc15bd01780eb9a607ac/passwd", "/etc/passwd"). + Bind("/tmp/fortify.1971/4a450b6596d7bc15bd01780eb9a607ac/group", "/etc/group"). + Tmpfs("/var/run/nscd", 8192), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + a := app.NewWithID(tc.id, new(stub)) + + if !t.Run("seal", func(t *testing.T) { + if err := a.Seal(tc.config); err != nil { + t.Errorf("Seal: error = %v", err) + } + }) { + return + } + + gotSys, gotBwrap := app.AppSystemBwrap(a) + + t.Run("compare sys", func(t *testing.T) { + if !gotSys.Equal(tc.wantSys) { + t.Errorf("Seal: sys = %#v, want %#v", + gotSys, tc.wantSys) + } + }) + + t.Run("compare bwrap", func(t *testing.T) { + if !reflect.DeepEqual(gotBwrap, tc.wantBwrap) { + t.Errorf("seal: bwrap = %#v, want %#v", + gotBwrap, tc.wantBwrap) + } + }) + }) + } +} + +// fs methods are not implemented using a real FS +// to help better understand filesystem access behaviour +type stub struct { + lookPathErr map[string]error + usernameErr map[string]error +} + +func (s *stub) Geteuid() int { + return 1971 +} + +func (s *stub) LookupEnv(key string) (string, bool) { + switch key { + case "SHELL": + return "/run/current-system/sw/bin/zsh", true + case "TERM": + return "xterm-256color", true + default: + panic(fmt.Sprintf("attempted to access unexpected environment variable %q", key)) + } +} + +func (s *stub) TempDir() string { + return "/tmp" +} + +func (s *stub) LookPath(file string) (string, error) { + if s.lookPathErr != nil { + if err, ok := s.lookPathErr[file]; ok { + return "", err + } + } + + switch file { + case "sudo": + return "/run/wrappers/bin/sudo", nil + default: + panic(fmt.Sprintf("attempted to look up unexpected executable %q", file)) + } +} + +func (s *stub) Executable() (string, error) { + return "/home/ophestra/.nix-profile/bin/fortify", nil +} + +func (s *stub) Lookup(username string) (*user.User, error) { + if s.usernameErr != nil { + if err, ok := s.usernameErr[username]; ok { + return nil, err + } + } + + switch username { + case "chronos": + return &user.User{ + Uid: "150", + Gid: "101", + Username: "chronos", + HomeDir: "/home/chronos", + }, nil + default: + return nil, user.UnknownUserError(username) + } +} + +func (s *stub) ReadDir(name string) ([]fs.DirEntry, error) { + switch name { + case "/": + return stubDirEntries("bin", "boot", "dev", "etc", "home", "lib", + "lib64", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var") + case "/run": + return stubDirEntries("agetty.reload", "binfmt", "booted-system", + "credentials", "cryptsetup", "current-system", "dbus", "host", "keys", + "libvirt", "libvirtd.pid", "lock", "log", "lvm", "mount", "NetworkManager", + "nginx", "nixos", "nscd", "opengl-driver", "pppd", "resolvconf", "sddm", + "store", "syncoid", "system", "systemd", "tmpfiles.d", "udev", "udisks2", + "user", "utmp", "virtlogd.pid", "wrappers", "zed.pid", "zed.state") + case "/etc": + return stubDirEntries("alsa", "bashrc", "binfmt.d", "dbus-1", "default", + "ethertypes", "fonts", "fstab", "fuse.conf", "group", "host.conf", "hostid", + "hostname", "hostname.CHECKSUM", "hosts", "inputrc", "ipsec.d", "issue", "kbd", + "libblockdev", "locale.conf", "localtime", "login.defs", "lsb-release", "lvm", + "machine-id", "man_db.conf", "modprobe.d", "modules-load.d", "mtab", "nanorc", + "netgroup", "NetworkManager", "nix", "nixos", "NIXOS", "nscd.conf", "nsswitch.conf", + "opensnitchd", "os-release", "pam", "pam.d", "passwd", "pipewire", "pki", "polkit-1", + "profile", "protocols", "qemu", "resolv.conf", "resolvconf.conf", "rpc", "samba", + "sddm.conf", "secureboot", "services", "set-environment", "shadow", "shells", "ssh", + "ssl", "static", "subgid", "subuid", "sudoers", "sysctl.d", "systemd", "terminfo", + "tmpfiles.d", "udev", "udisks2", "UPower", "vconsole.conf", "X11", "zfs", "zinputrc", + "zoneinfo", "zprofile", "zshenv", "zshrc") + default: + panic(fmt.Sprintf("attempted to read unexpected directory %q", name)) + } +} + +func (s *stub) Stat(name string) (fs.FileInfo, error) { + switch name { + case "/var/run/nscd": + return nil, nil + default: + panic(fmt.Sprintf("attempted to stat unexpected path %q", name)) + } +} + +func (s *stub) Open(name string) (fs.File, error) { + switch name { + default: + panic(fmt.Sprintf("attempted to open unexpected file %q", name)) + } +} + +func (s *stub) Exit(code int) { + panic("called exit on stub with code " + strconv.Itoa(code)) +} + +func (s *stub) Paths() internal.Paths { + return internal.Paths{ + SharePath: "/tmp/fortify.1971", + RuntimePath: "/run/user/1971", + RunDirPath: "/run/user/1971/fortify", + } +} + +func stubDirEntries(names ...string) (e []fs.DirEntry, err error) { + e = make([]fs.DirEntry, len(names)) + for i, name := range names { + e[i] = stubDirEntryPath(name) + } + return +} + +type stubDirEntryPath string + +func (p stubDirEntryPath) Name() string { + return string(p) +} + +func (p stubDirEntryPath) IsDir() bool { + panic("attempted to call IsDir") +} + +func (p stubDirEntryPath) Type() fs.FileMode { + panic("attempted to call Type") +} + +func (p stubDirEntryPath) Info() (fs.FileInfo, error) { + panic("attempted to call Info") +} diff --git a/internal/app/export_test.go b/internal/app/export_test.go new file mode 100644 index 0000000..8e52468 --- /dev/null +++ b/internal/app/export_test.go @@ -0,0 +1,19 @@ +package app + +import ( + "git.ophivana.moe/security/fortify/helper/bwrap" + "git.ophivana.moe/security/fortify/internal" + "git.ophivana.moe/security/fortify/internal/system" +) + +func NewWithID(id ID, os internal.System) App { + a := new(app) + a.id = &id + a.os = os + return a +} + +func AppSystemBwrap(a App) (*system.I, *bwrap.Config) { + v := a.(*app) + return v.seal.sys.I, v.seal.sys.bwrap +}