294 lines
8.2 KiB
Go
294 lines
8.2 KiB
Go
//go:build linux
|
|
|
|
package securefile
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"syscall"
|
|
"testing"
|
|
)
|
|
|
|
func TestOpenDirAndReadFileAcceptProtectedRegularFile(t *testing.T) {
|
|
root := t.TempDir()
|
|
path := filepath.Join(root, "record.json")
|
|
want := []byte(`{"record":"synthetic"}`)
|
|
writeFile(t, path, want, 0o640)
|
|
|
|
dir, err := OpenDir(root)
|
|
if err != nil {
|
|
t.Fatalf("OpenDir() error = %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = dir.Close() })
|
|
|
|
got, err := dir.ReadFile("record.json", 4096)
|
|
if err != nil {
|
|
t.Fatalf("ReadFile() error = %v", err)
|
|
}
|
|
if !bytes.Equal(got, want) {
|
|
t.Fatalf("ReadFile() = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestProtectedPathsRejectSymlinks(t *testing.T) {
|
|
t.Run("root", func(t *testing.T) {
|
|
parent := t.TempDir()
|
|
target := t.TempDir()
|
|
root := filepath.Join(parent, "registry")
|
|
if err := os.Symlink(target, root); err != nil {
|
|
t.Fatalf("Symlink() error = %v", err)
|
|
}
|
|
if _, err := OpenDir(root); err == nil {
|
|
t.Fatal("OpenDir() error = nil, want symlink refusal")
|
|
}
|
|
})
|
|
|
|
t.Run("directory", func(t *testing.T) {
|
|
root := t.TempDir()
|
|
target := t.TempDir()
|
|
if err := os.Symlink(target, filepath.Join(root, "active")); err != nil {
|
|
t.Fatalf("Symlink() error = %v", err)
|
|
}
|
|
dir, err := OpenDir(root)
|
|
if err != nil {
|
|
t.Fatalf("OpenDir() error = %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = dir.Close() })
|
|
if _, err := dir.OpenDir("active"); err == nil {
|
|
t.Fatal("OpenDir(active) error = nil, want symlink refusal")
|
|
}
|
|
})
|
|
|
|
t.Run("record", func(t *testing.T) {
|
|
root := t.TempDir()
|
|
dir, err := OpenDir(root)
|
|
if err != nil {
|
|
t.Fatalf("OpenDir() error = %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = dir.Close() })
|
|
target := filepath.Join(root, "target.json")
|
|
writeFile(t, target, []byte(`{"safe":false}`), 0o640)
|
|
if err := os.Symlink(target, filepath.Join(root, "record.json")); err != nil {
|
|
t.Fatalf("Symlink() error = %v", err)
|
|
}
|
|
if _, err := dir.ReadFile("record.json", 4096); err == nil {
|
|
t.Fatal("ReadFile() error = nil, want symlink refusal")
|
|
}
|
|
})
|
|
|
|
t.Run("secret", func(t *testing.T) {
|
|
root := t.TempDir()
|
|
target := filepath.Join(root, "target.secret")
|
|
writeFile(t, target, []byte("synthetic-secret"), 0o600)
|
|
secret := filepath.Join(root, "secret")
|
|
if err := os.Symlink(target, secret); err != nil {
|
|
t.Fatalf("Symlink() error = %v", err)
|
|
}
|
|
if _, err := ReadSecret(secret, 128); err == nil {
|
|
t.Fatal("ReadSecret() error = nil, want symlink refusal")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestProtectedPathsRejectUnsafeModes(t *testing.T) {
|
|
t.Run("root", func(t *testing.T) {
|
|
root := t.TempDir()
|
|
if err := os.Chmod(root, 0o770); err != nil {
|
|
t.Fatalf("Chmod() error = %v", err)
|
|
}
|
|
if _, err := OpenDir(root); err == nil {
|
|
t.Fatal("OpenDir() error = nil, want unsafe mode refusal")
|
|
}
|
|
})
|
|
|
|
t.Run("child directory", func(t *testing.T) {
|
|
root := t.TempDir()
|
|
child := filepath.Join(root, "active")
|
|
if err := os.Mkdir(child, 0o770); err != nil {
|
|
t.Fatalf("Mkdir() error = %v", err)
|
|
}
|
|
if err := os.Chmod(child, 0o770); err != nil {
|
|
t.Fatalf("Chmod() error = %v", err)
|
|
}
|
|
dir, err := OpenDir(root)
|
|
if err != nil {
|
|
t.Fatalf("OpenDir() error = %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = dir.Close() })
|
|
if _, err := dir.OpenDir("active"); err == nil {
|
|
t.Fatal("OpenDir(active) error = nil, want unsafe mode refusal")
|
|
}
|
|
})
|
|
|
|
t.Run("record", func(t *testing.T) {
|
|
root := t.TempDir()
|
|
path := filepath.Join(root, "record.json")
|
|
writeFile(t, path, []byte(`{"unsafe":true}`), 0o660)
|
|
dir, err := OpenDir(root)
|
|
if err != nil {
|
|
t.Fatalf("OpenDir() error = %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = dir.Close() })
|
|
if _, err := dir.ReadFile("record.json", 4096); err == nil {
|
|
t.Fatal("ReadFile() error = nil, want unsafe mode refusal")
|
|
}
|
|
})
|
|
|
|
t.Run("secret", func(t *testing.T) {
|
|
root := t.TempDir()
|
|
secret := filepath.Join(root, "secret")
|
|
writeFile(t, secret, []byte("synthetic-secret"), 0o640)
|
|
if _, err := ReadSecret(secret, 128); err == nil {
|
|
t.Fatal("ReadSecret() error = nil, want non-0600 refusal")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestReadFileRejectsOversizeContent(t *testing.T) {
|
|
root := t.TempDir()
|
|
path := filepath.Join(root, "record.json")
|
|
writeFile(t, path, bytes.Repeat([]byte{'a'}, 4097), 0o640)
|
|
dir, err := OpenDir(root)
|
|
if err != nil {
|
|
t.Fatalf("OpenDir() error = %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = dir.Close() })
|
|
|
|
if _, err := dir.ReadFile("record.json", 4096); err == nil {
|
|
t.Fatal("ReadFile() error = nil, want bounded-read refusal")
|
|
}
|
|
}
|
|
|
|
func TestReadSecretAndCreateSecretUse0600AndExclusiveCreate(t *testing.T) {
|
|
root := t.TempDir()
|
|
input := filepath.Join(root, "input.secret")
|
|
want := []byte("synthetic-secret")
|
|
writeFile(t, input, want, 0o600)
|
|
|
|
got, err := ReadSecret(input, 128)
|
|
if err != nil {
|
|
t.Fatalf("ReadSecret() error = %v", err)
|
|
}
|
|
if !bytes.Equal(got, want) {
|
|
t.Fatalf("ReadSecret() = %q, want %q", got, want)
|
|
}
|
|
|
|
output := filepath.Join(root, "output.secret")
|
|
file, err := CreateSecret(output)
|
|
if err != nil {
|
|
t.Fatalf("CreateSecret() error = %v", err)
|
|
}
|
|
if _, err := file.Write(want); err != nil {
|
|
_ = file.Close()
|
|
t.Fatalf("Write() error = %v", err)
|
|
}
|
|
if err := file.Sync(); err != nil {
|
|
_ = file.Close()
|
|
t.Fatalf("Sync() error = %v", err)
|
|
}
|
|
if err := file.Close(); err != nil {
|
|
t.Fatalf("Close() error = %v", err)
|
|
}
|
|
info, err := os.Stat(output)
|
|
if err != nil {
|
|
t.Fatalf("Stat() error = %v", err)
|
|
}
|
|
if got, want := info.Mode().Perm(), os.FileMode(0o600); got != want {
|
|
t.Fatalf("output mode = %04o, want %04o", got, want)
|
|
}
|
|
if _, err := CreateSecret(output); err == nil {
|
|
t.Fatal("CreateSecret(existing) error = nil, want exclusive-create refusal")
|
|
} else if !errors.Is(err, os.ErrExist) {
|
|
t.Fatalf("CreateSecret(existing) error = %v, want os.ErrExist", err)
|
|
}
|
|
}
|
|
|
|
func TestLockSharedAllowsReadersAndBlocksExclusiveWriter(t *testing.T) {
|
|
root := t.TempDir()
|
|
first, err := OpenDir(root)
|
|
if err != nil {
|
|
t.Fatalf("OpenDir(first) error = %v", err)
|
|
}
|
|
defer first.Close()
|
|
second, err := OpenDir(root)
|
|
if err != nil {
|
|
t.Fatalf("OpenDir(second) error = %v", err)
|
|
}
|
|
defer second.Close()
|
|
third, err := OpenDir(root)
|
|
if err != nil {
|
|
t.Fatalf("OpenDir(third) error = %v", err)
|
|
}
|
|
defer third.Close()
|
|
if first.fd == second.fd || second.fd == third.fd || first.fd == third.fd {
|
|
t.Fatal("OpenDir() reused a descriptor; want separate lock file descriptions")
|
|
}
|
|
|
|
type sharedLocker interface {
|
|
LockShared(string) (*Lock, error)
|
|
}
|
|
firstLocker, ok := any(first).(sharedLocker)
|
|
if !ok {
|
|
t.Fatal("Dir lacks shared advisory locking")
|
|
}
|
|
secondLocker, ok := any(second).(sharedLocker)
|
|
if !ok {
|
|
t.Fatal("Dir lacks shared advisory locking")
|
|
}
|
|
|
|
firstShared, err := firstLocker.LockShared("registry.lock")
|
|
if err != nil {
|
|
t.Fatalf("LockShared(first) error = %v", err)
|
|
}
|
|
defer firstShared.Close()
|
|
secondShared, err := secondLocker.LockShared("registry.lock")
|
|
if err != nil {
|
|
t.Fatalf("LockShared(second) error = %v", err)
|
|
}
|
|
defer secondShared.Close()
|
|
|
|
probe, err := os.OpenFile(filepath.Join(root, "registry.lock"), os.O_RDWR, 0)
|
|
if err != nil {
|
|
t.Fatalf("OpenFile(lock probe) error = %v", err)
|
|
}
|
|
if err := syscall.Flock(int(probe.Fd()), syscall.LOCK_EX|syscall.LOCK_NB); err == nil {
|
|
_ = syscall.Flock(int(probe.Fd()), syscall.LOCK_UN)
|
|
_ = probe.Close()
|
|
t.Fatal("exclusive lock acquired while separate shared locks were held")
|
|
} else if !errors.Is(err, syscall.EWOULDBLOCK) && !errors.Is(err, syscall.EAGAIN) {
|
|
_ = probe.Close()
|
|
t.Fatalf("nonblocking exclusive lock error = %v, want EWOULDBLOCK", err)
|
|
}
|
|
if err := probe.Close(); err != nil {
|
|
t.Fatalf("Close(lock probe) error = %v", err)
|
|
}
|
|
|
|
if err := secondShared.Close(); err != nil {
|
|
t.Fatalf("Close(second shared) error = %v", err)
|
|
}
|
|
secondShared = nil
|
|
if err := firstShared.Close(); err != nil {
|
|
t.Fatalf("Close(first shared) error = %v", err)
|
|
}
|
|
firstShared = nil
|
|
exclusive, err := third.Lock("registry.lock")
|
|
if err != nil {
|
|
t.Fatalf("Lock(exclusive after shared release) error = %v", err)
|
|
}
|
|
if err := exclusive.Close(); err != nil {
|
|
t.Fatalf("Close(exclusive) error = %v", err)
|
|
}
|
|
}
|
|
|
|
func writeFile(t *testing.T, path string, data []byte, mode os.FileMode) {
|
|
t.Helper()
|
|
if err := os.WriteFile(path, data, mode); err != nil {
|
|
t.Fatalf("WriteFile(%q) error = %v", path, err)
|
|
}
|
|
if err := os.Chmod(path, mode); err != nil {
|
|
t.Fatalf("Chmod(%q) error = %v", path, err)
|
|
}
|
|
}
|