Files
ThothII/tools/dwh-auth/internal/securefile/securefile_linux.go

568 lines
15 KiB
Go

//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 0640 lock file and acquires an exclusive
// advisory lock. Close releases the lock and descriptor.
func (d *Dir) Lock(name string) (*Lock, error) {
return d.lock(name, syscall.LOCK_EX)
}
// LockShared opens an existing protected 0640 lock read-only and acquires a
// shared advisory lock. It never creates or changes registry state.
func (d *Dir) LockShared(name string) (*Lock, 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, syscall.O_RDONLY|syscall.O_NOFOLLOW|syscall.O_CLOEXEC, 0)
if err != nil {
return nil, err
}
if err := validateRegularFD(fd, 0o640); 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_SH); err != nil {
_ = syscall.Close(fd)
return nil, err
}
return &Lock{fd: fd}, nil
}
func (d *Dir) lock(name string, lockMode int) (*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, 0o640)
if errors.Is(err, syscall.EEXIST) {
continue
}
if err != nil {
return nil, err
}
if err := syscall.Fchmod(fd, 0o640); 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, 0o640); 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, lockMode); 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&0o5022 != 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)