feat: add protected DWH credential registry
This commit is contained in:
@@ -0,0 +1,535 @@
|
||||
//go:build linux
|
||||
|
||||
// Package securefile provides Linux-only, no-follow filesystem primitives for
|
||||
// protected credential material.
|
||||
package securefile
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrUnsafe identifies a path, mode, or file type that cannot be trusted.
|
||||
ErrUnsafe = errors.New("unsafe filesystem object")
|
||||
// ErrTooLarge identifies a file that exceeds its caller-provided bound.
|
||||
ErrTooLarge = errors.New("file exceeds size bound")
|
||||
// ErrChanged identifies a file whose size changed while it was read.
|
||||
ErrChanged = errors.New("file changed during read")
|
||||
)
|
||||
|
||||
// Dir is an open, protected directory descriptor. Operations are rooted at the
|
||||
// descriptor rather than reopening attacker-controlled path prefixes.
|
||||
type Dir struct {
|
||||
fd int
|
||||
path string
|
||||
}
|
||||
|
||||
// Lock serializes cooperating writers using an advisory lock on a protected
|
||||
// regular file.
|
||||
type Lock struct {
|
||||
fd int
|
||||
}
|
||||
|
||||
// OpenDir opens an absolute directory without following any path component and
|
||||
// rejects unsafe modes on the protected final directory.
|
||||
func OpenDir(path string) (*Dir, error) {
|
||||
clean, err := absolutePath(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fd, err := openDirectoryPath(clean)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validateDirectoryFD(fd); err != nil {
|
||||
_ = syscall.Close(fd)
|
||||
return nil, err
|
||||
}
|
||||
if err := compareWithLstat(fd, clean, true); err != nil {
|
||||
_ = syscall.Close(fd)
|
||||
return nil, err
|
||||
}
|
||||
return &Dir{fd: fd, path: clean}, nil
|
||||
}
|
||||
|
||||
// Close closes the directory descriptor.
|
||||
func (d *Dir) Close() error {
|
||||
if d == nil || d.fd < 0 {
|
||||
return nil
|
||||
}
|
||||
err := syscall.Close(d.fd)
|
||||
d.fd = -1
|
||||
return err
|
||||
}
|
||||
|
||||
// OpenDir opens one direct child directory without following it.
|
||||
func (d *Dir) OpenDir(name string) (*Dir, error) {
|
||||
if err := validName(name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := d.check(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fd, err := syscall.Openat(d.fd, name, directoryOpenFlags, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validateDirectoryFD(fd); err != nil {
|
||||
_ = syscall.Close(fd)
|
||||
return nil, err
|
||||
}
|
||||
path := filepath.Join(d.path, name)
|
||||
if err := compareWithLstat(fd, path, true); err != nil {
|
||||
_ = syscall.Close(fd)
|
||||
return nil, err
|
||||
}
|
||||
return &Dir{fd: fd, path: path}, nil
|
||||
}
|
||||
|
||||
// OpenOrCreateDir opens a protected child directory or creates it with a
|
||||
// restrictive mode. A collision is reopened and fully revalidated.
|
||||
func (d *Dir) OpenOrCreateDir(name string, mode os.FileMode) (*Dir, error) {
|
||||
child, err := d.OpenDir(name)
|
||||
if err == nil {
|
||||
return child, nil
|
||||
}
|
||||
if !errors.Is(err, syscall.ENOENT) {
|
||||
return nil, err
|
||||
}
|
||||
if err := validMode(mode); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := d.check(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := syscall.Mkdirat(d.fd, name, uint32(mode.Perm())); err != nil && !errors.Is(err, syscall.EEXIST) {
|
||||
return nil, err
|
||||
}
|
||||
child, err = d.OpenDir(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := d.Sync(); err != nil {
|
||||
_ = child.Close()
|
||||
return nil, err
|
||||
}
|
||||
return child, nil
|
||||
}
|
||||
|
||||
// ReadFile reads one protected regular child file, bounded by max bytes. It
|
||||
// verifies file type and mode before and after the read and detects size races.
|
||||
func (d *Dir) ReadFile(name string, max int) ([]byte, error) {
|
||||
return d.readFile(name, max, nil)
|
||||
}
|
||||
|
||||
// ReadSecret reads an absolute secret input whose final parent is protected and
|
||||
// whose file mode is exactly 0600.
|
||||
func ReadSecret(path string, max int) ([]byte, error) {
|
||||
clean, err := absolutePath(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parent := filepath.Dir(clean)
|
||||
name := filepath.Base(clean)
|
||||
if err := validName(name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dir, err := OpenDir(parent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer dir.Close()
|
||||
mode := uint32(0o600)
|
||||
return dir.readFile(name, max, &mode)
|
||||
}
|
||||
|
||||
// CreateSecret creates a new absolute output file with O_CREAT|O_EXCL and an
|
||||
// exact mode of 0600. The caller owns and must close the returned file.
|
||||
func CreateSecret(path string) (*os.File, error) {
|
||||
clean, err := absolutePath(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parent := filepath.Dir(clean)
|
||||
name := filepath.Base(clean)
|
||||
if err := validName(name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dir, err := OpenDir(parent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer dir.Close()
|
||||
return dir.CreateExclusive(name, 0o600)
|
||||
}
|
||||
|
||||
// CreateExclusive creates one direct child with O_CREAT|O_EXCL|O_NOFOLLOW and
|
||||
// the exact requested restrictive mode. The caller owns and must close it.
|
||||
func (d *Dir) CreateExclusive(name string, mode os.FileMode) (*os.File, error) {
|
||||
if err := validName(name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validMode(mode); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := d.check(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fd, err := syscall.Openat(d.fd, name, syscall.O_WRONLY|syscall.O_CREAT|syscall.O_EXCL|syscall.O_NOFOLLOW|syscall.O_CLOEXEC, uint32(mode.Perm()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := syscall.Fchmod(fd, uint32(mode.Perm())); err != nil {
|
||||
_ = syscall.Close(fd)
|
||||
return nil, err
|
||||
}
|
||||
if err := validateRegularFD(fd, uint32(mode.Perm())); err != nil {
|
||||
_ = syscall.Close(fd)
|
||||
return nil, err
|
||||
}
|
||||
return os.NewFile(uintptr(fd), filepath.Join(d.path, name)), nil
|
||||
}
|
||||
|
||||
// Exists reports whether a direct protected regular child exists. An unsafe
|
||||
// collision is an error rather than an absent file.
|
||||
func (d *Dir) Exists(name string) (bool, error) {
|
||||
if err := validName(name); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := d.check(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
fd, err := syscall.Openat(d.fd, name, syscall.O_RDONLY|syscall.O_NOFOLLOW|syscall.O_CLOEXEC, 0)
|
||||
if errors.Is(err, syscall.ENOENT) {
|
||||
return false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer syscall.Close(fd)
|
||||
if err := validateRegularFD(fd, 0); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := compareWithLstat(fd, filepath.Join(d.path, name), false); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Rename atomically renames a direct child into another protected directory.
|
||||
// Callers that need no-replace semantics must serialize writers and verify the
|
||||
// destination is absent before calling Rename.
|
||||
func (d *Dir) Rename(oldName string, destination *Dir, newName string) error {
|
||||
if err := validName(oldName); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validName(newName); err != nil {
|
||||
return err
|
||||
}
|
||||
if destination == nil {
|
||||
return fmt.Errorf("%w: nil destination", ErrUnsafe)
|
||||
}
|
||||
if err := d.check(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := destination.check(); err != nil {
|
||||
return err
|
||||
}
|
||||
return syscall.Renameat(d.fd, oldName, destination.fd, newName)
|
||||
}
|
||||
|
||||
// Remove removes one direct child file without following it.
|
||||
func (d *Dir) Remove(name string) error {
|
||||
if err := validName(name); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.check(); err != nil {
|
||||
return err
|
||||
}
|
||||
return syscall.Unlinkat(d.fd, name)
|
||||
}
|
||||
|
||||
// Sync makes prior directory entry changes durable.
|
||||
func (d *Dir) Sync() error {
|
||||
if err := d.check(); err != nil {
|
||||
return err
|
||||
}
|
||||
return syscall.Fsync(d.fd)
|
||||
}
|
||||
|
||||
// Names returns direct entry names from the protected directory descriptor.
|
||||
func (d *Dir) Names() ([]string, error) {
|
||||
if err := d.check(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fd, err := syscall.Openat(d.fd, ".", directoryOpenFlags, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
file := os.NewFile(uintptr(fd), d.path)
|
||||
entries, err := file.ReadDir(-1)
|
||||
closeErr := file.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if closeErr != nil {
|
||||
return nil, closeErr
|
||||
}
|
||||
names := make([]string, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
names = append(names, entry.Name())
|
||||
}
|
||||
return names, nil
|
||||
}
|
||||
|
||||
// Lock opens or creates a protected 0600 lock file and acquires an exclusive
|
||||
// advisory lock. Close releases the lock and descriptor.
|
||||
func (d *Dir) Lock(name string) (*Lock, error) {
|
||||
if err := validName(name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := d.check(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for {
|
||||
fd, err := syscall.Openat(d.fd, name, syscall.O_RDWR|syscall.O_NOFOLLOW|syscall.O_CLOEXEC, 0)
|
||||
if errors.Is(err, syscall.ENOENT) {
|
||||
fd, err = syscall.Openat(d.fd, name, syscall.O_RDWR|syscall.O_CREAT|syscall.O_EXCL|syscall.O_NOFOLLOW|syscall.O_CLOEXEC, 0o600)
|
||||
if errors.Is(err, syscall.EEXIST) {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := syscall.Fchmod(fd, 0o600); err != nil {
|
||||
_ = syscall.Close(fd)
|
||||
return nil, err
|
||||
}
|
||||
if err := d.Sync(); err != nil {
|
||||
_ = syscall.Close(fd)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validateRegularFD(fd, 0o600); err != nil {
|
||||
_ = syscall.Close(fd)
|
||||
return nil, err
|
||||
}
|
||||
if err := compareWithLstat(fd, filepath.Join(d.path, name), false); err != nil {
|
||||
_ = syscall.Close(fd)
|
||||
return nil, err
|
||||
}
|
||||
if err := syscall.Flock(fd, syscall.LOCK_EX); err != nil {
|
||||
_ = syscall.Close(fd)
|
||||
return nil, err
|
||||
}
|
||||
return &Lock{fd: fd}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Close releases an advisory lock and closes its descriptor.
|
||||
func (l *Lock) Close() error {
|
||||
if l == nil || l.fd < 0 {
|
||||
return nil
|
||||
}
|
||||
unlockErr := syscall.Flock(l.fd, syscall.LOCK_UN)
|
||||
closeErr := syscall.Close(l.fd)
|
||||
l.fd = -1
|
||||
if unlockErr != nil {
|
||||
return unlockErr
|
||||
}
|
||||
return closeErr
|
||||
}
|
||||
|
||||
func (d *Dir) readFile(name string, max int, exactMode *uint32) ([]byte, error) {
|
||||
if err := validName(name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if max < 0 {
|
||||
return nil, fmt.Errorf("%w: negative size bound", ErrUnsafe)
|
||||
}
|
||||
if err := d.check(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fd, err := syscall.Openat(d.fd, name, syscall.O_RDONLY|syscall.O_NOFOLLOW|syscall.O_CLOEXEC, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer syscall.Close(fd)
|
||||
|
||||
var before syscall.Stat_t
|
||||
if err := syscall.Fstat(fd, &before); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validateRegularStat(&before, exactMode); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := compareWithLstat(fd, filepath.Join(d.path, name), false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if before.Size < 0 || before.Size > int64(max) {
|
||||
return nil, fmt.Errorf("%w: %d bytes", ErrTooLarge, before.Size)
|
||||
}
|
||||
size := int(before.Size)
|
||||
data := make([]byte, size+1)
|
||||
n := 0
|
||||
for n < len(data) {
|
||||
read, readErr := syscall.Read(fd, data[n:])
|
||||
if read > 0 {
|
||||
n += read
|
||||
}
|
||||
if errors.Is(readErr, syscall.EINTR) {
|
||||
continue
|
||||
}
|
||||
if readErr != nil {
|
||||
return nil, readErr
|
||||
}
|
||||
if read == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var after syscall.Stat_t
|
||||
if err := syscall.Fstat(fd, &after); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validateRegularStat(&after, exactMode); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if before.Dev != after.Dev || before.Ino != after.Ino || before.Size != after.Size {
|
||||
return nil, ErrChanged
|
||||
}
|
||||
if n != size {
|
||||
return nil, ErrChanged
|
||||
}
|
||||
return data[:n], nil
|
||||
}
|
||||
|
||||
func (d *Dir) check() error {
|
||||
if d == nil || d.fd < 0 {
|
||||
return fmt.Errorf("%w: closed directory", ErrUnsafe)
|
||||
}
|
||||
return validateDirectoryFD(d.fd)
|
||||
}
|
||||
|
||||
func absolutePath(path string) (string, error) {
|
||||
if !filepath.IsAbs(path) {
|
||||
return "", fmt.Errorf("%w: path must be absolute", ErrUnsafe)
|
||||
}
|
||||
return filepath.Clean(path), nil
|
||||
}
|
||||
|
||||
func openDirectoryPath(path string) (int, error) {
|
||||
fd, err := syscall.Open("/", directoryOpenFlags, 0)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
if path == "/" {
|
||||
return fd, nil
|
||||
}
|
||||
for _, part := range strings.Split(strings.TrimPrefix(path, "/"), "/") {
|
||||
next, err := syscall.Openat(fd, part, directoryOpenFlags, 0)
|
||||
_ = syscall.Close(fd)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
var stat syscall.Stat_t
|
||||
if err := syscall.Fstat(next, &stat); err != nil {
|
||||
_ = syscall.Close(next)
|
||||
return -1, err
|
||||
}
|
||||
if stat.Mode&syscall.S_IFMT != syscall.S_IFDIR {
|
||||
_ = syscall.Close(next)
|
||||
return -1, fmt.Errorf("%w: non-directory path component", ErrUnsafe)
|
||||
}
|
||||
fd = next
|
||||
}
|
||||
return fd, nil
|
||||
}
|
||||
|
||||
func compareWithLstat(fd int, path string, directory bool) error {
|
||||
var opened syscall.Stat_t
|
||||
if err := syscall.Fstat(fd, &opened); err != nil {
|
||||
return err
|
||||
}
|
||||
var linked syscall.Stat_t
|
||||
if err := syscall.Lstat(path, &linked); err != nil {
|
||||
return err
|
||||
}
|
||||
if directory {
|
||||
if linked.Mode&syscall.S_IFMT != syscall.S_IFDIR {
|
||||
return fmt.Errorf("%w: path is not a directory", ErrUnsafe)
|
||||
}
|
||||
} else if linked.Mode&syscall.S_IFMT != syscall.S_IFREG {
|
||||
return fmt.Errorf("%w: path is not a regular file", ErrUnsafe)
|
||||
}
|
||||
if opened.Dev != linked.Dev || opened.Ino != linked.Ino {
|
||||
return fmt.Errorf("%w: path changed while opening", ErrUnsafe)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateDirectoryFD(fd int) error {
|
||||
var stat syscall.Stat_t
|
||||
if err := syscall.Fstat(fd, &stat); err != nil {
|
||||
return err
|
||||
}
|
||||
if stat.Mode&syscall.S_IFMT != syscall.S_IFDIR {
|
||||
return fmt.Errorf("%w: not a directory", ErrUnsafe)
|
||||
}
|
||||
if stat.Mode&0o7022 != 0 {
|
||||
return fmt.Errorf("%w: unsafe directory mode %04o", ErrUnsafe, stat.Mode&0o7777)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateRegularFD(fd int, exactMode uint32) error {
|
||||
var stat syscall.Stat_t
|
||||
if err := syscall.Fstat(fd, &stat); err != nil {
|
||||
return err
|
||||
}
|
||||
var required *uint32
|
||||
if exactMode != 0 {
|
||||
required = &exactMode
|
||||
}
|
||||
return validateRegularStat(&stat, required)
|
||||
}
|
||||
|
||||
func validateRegularStat(stat *syscall.Stat_t, exactMode *uint32) error {
|
||||
if stat.Mode&syscall.S_IFMT != syscall.S_IFREG {
|
||||
return fmt.Errorf("%w: not a regular file", ErrUnsafe)
|
||||
}
|
||||
if stat.Mode&0o7022 != 0 {
|
||||
return fmt.Errorf("%w: unsafe file mode %04o", ErrUnsafe, stat.Mode&0o7777)
|
||||
}
|
||||
if exactMode != nil && stat.Mode&0o777 != *exactMode {
|
||||
return fmt.Errorf("%w: file mode %04o is not %04o", ErrUnsafe, stat.Mode&0o777, *exactMode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validMode(mode os.FileMode) error {
|
||||
if mode&^os.FileMode(0o777) != 0 || mode.Perm()&0o022 != 0 {
|
||||
return fmt.Errorf("%w: unsafe creation mode %04o", ErrUnsafe, mode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validName(name string) error {
|
||||
if name == "" || name == "." || name == ".." || strings.Contains(name, "/") || strings.ContainsRune(name, 0) {
|
||||
return fmt.Errorf("%w: invalid path component", ErrUnsafe)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const directoryOpenFlags = syscall.O_RDONLY | syscall.O_DIRECTORY | syscall.O_NOFOLLOW | syscall.O_CLOEXEC
|
||||
|
||||
var _ io.Closer = (*Dir)(nil)
|
||||
@@ -0,0 +1,215 @@
|
||||
//go:build linux
|
||||
|
||||
package securefile
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"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 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user