Scert/backend/internal/nginxactivation/identity_linux_test.go

188 lines
5.7 KiB
Go

package nginxactivation
import (
"bufio"
"context"
"fmt"
"io"
"net"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
"testing"
"time"
)
func identityChild(t *testing.T, mode string) {
t.Helper()
path := os.Getenv("SCERT_IDENTITY_TEST_SOCKET")
if mode == "server" {
listener, err := net.ListenUnix("unix", &net.UnixAddr{Name: path, Net: "unix"})
if err != nil {
t.Fatal(err)
}
listener.SetUnlinkOnClose(false)
defer listener.Close()
_ = listener.SetDeadline(time.Now().Add(5 * time.Second))
fmt.Println("LISTENING")
connection, err := listener.AcceptUnix()
if err != nil {
t.Fatal(err)
}
defer connection.Close()
_ = connection.SetDeadline(time.Now().Add(5 * time.Second))
var data [1]byte
n, err := connection.Read(data[:])
if n != 0 || err != io.EOF {
t.Fatal("客户端向不可信服务发送了数据", n, err)
}
fmt.Println("IDENTITY_VERIFIED")
return
}
connection, err := net.DialTimeout("unix", path, time.Second)
if err != nil {
t.Fatal("未到达内核身份检查边界", err)
}
defer connection.Close()
_ = connection.SetDeadline(time.Now().Add(3 * time.Second))
err = writeFrame(connection, request{Version: protocolVersion, Action: "state"})
var result response
if err == nil {
err = readFrame(connection, &result)
}
if mode == "allowed" {
if err != nil || result.Code != "ok" || result.State == nil || result.State.Status != "idle" {
t.Fatal("合法非特权进程未通过身份校验", err)
}
} else if err == nil {
t.Fatal("非授权进程获得了响应")
}
fmt.Println("IDENTITY_VERIFIED")
}
func TestRootPeerIdentity(t *testing.T) {
if mode := os.Getenv("SCERT_IDENTITY_TEST_CHILD"); mode != "" {
identityChild(t, mode)
return
}
if os.Getenv("SCERT_TEST_ROOT_IPC") != "1" {
t.Skip("仅在显式启用的 root 隔离测试单元内执行不同 UID 测试")
}
if os.Geteuid() != 0 {
t.Fatal("不同 UID 测试要求隔离单元以 root 运行")
}
base := t.TempDir()
for _, path := range []string{base, filepath.Dir(base)} {
if err := os.Chmod(path, 0o755); err != nil {
t.Fatal(err)
}
}
// 为降权子进程复制独立测试程序,避免放宽正式目录的访问权限。
source, err := os.Open(os.Args[0])
if err != nil {
t.Fatal(err)
}
defer source.Close()
executable := filepath.Join(base, "identity.test")
copy, err := os.OpenFile(executable, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o755)
if err != nil {
t.Fatal(err)
}
_, copyErr := io.Copy(copy, source)
closeErr := copy.Close()
if copyErr != nil || closeErr != nil {
t.Fatal(copyErr, closeErr)
}
command := func(mode, path string, uid uint32) (*exec.Cmd, context.CancelFunc) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
cmd := exec.CommandContext(ctx, executable, "-test.run=^TestRootPeerIdentity$")
cmd.Env = append(os.Environ(), "SCERT_IDENTITY_TEST_CHILD="+mode, "SCERT_IDENTITY_TEST_SOCKET="+path)
cmd.SysProcAttr = &syscall.SysProcAttr{Credential: &syscall.Credential{Uid: uid, Gid: uid}}
return cmd, cancel
}
t.Run("caller", func(t *testing.T) {
f := newRotationFixture(t)
path := filepath.Join(base, "service.sock")
listener, err := net.ListenUnix("unix", &net.UnixAddr{Name: path, Net: "unix"})
if err != nil {
t.Fatal(err)
}
listener.SetUnlinkOnClose(false)
// 此用例故意放宽 inode 权限,只验证内核 UID 拒绝,而非文件权限拒绝。
if err := os.Chmod(path, 0o666); err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithCancel(context.Background())
done := make(chan error, 1)
service := LocalService{Rotation: f.rotation, AllowedUID: 65534}
go func() { done <- service.Serve(ctx, listener) }()
defer stopLocalService(t, cancel, done)
for _, peer := range []struct {
mode string
uid uint32
}{{"allowed", 65534}, {"denied", 65533}} {
cmd, stop := command(peer.mode, path, peer.uid)
output, err := cmd.CombinedOutput()
stop()
if err != nil || !strings.Contains(string(output), "IDENTITY_VERIFIED") {
t.Fatalf("UID %d 身份验证失败:%s, %v", peer.uid, output, err)
}
}
if state, err := f.rotation.State(context.Background()); err != nil || state.Status != "idle" {
t.Fatal("身份验证改变了事务状态", state, err)
}
})
t.Run("server", func(t *testing.T) {
parent := filepath.Join(base, "server")
if err := os.Mkdir(parent, 0o755); err != nil {
t.Fatal(err)
}
if err := os.Chmod(parent, 0o777); err != nil {
t.Fatal(err)
}
path := filepath.Join(parent, "service.sock")
cmd, cancel := command("server", path, 65534)
defer cancel()
output, err := cmd.StdoutPipe()
if err != nil {
t.Fatal(err)
}
var stderr strings.Builder
cmd.Stderr = &stderr
if err := cmd.Start(); err != nil {
t.Fatal(err)
}
defer func() { cancel(); _ = cmd.Wait() }()
scanner := bufio.NewScanner(output)
if !scanner.Scan() || scanner.Text() != "LISTENING" {
t.Fatal("身份模拟服务未启动")
}
// inode 看起来由 root 管理,但真正的监听创建者仍是非特权进程。
if err := os.Chown(path, 0, 0); err != nil {
t.Fatal(err)
}
if err := os.Chmod(path, 0o600); err != nil {
t.Fatal(err)
}
if err := os.Chmod(parent, 0o755); err != nil {
t.Fatal(err)
}
if err := validateSocket(path, 0); err != nil {
t.Fatal("未到达服务内核身份检查", err)
}
_, err = (Client{SocketPath: path}).State(context.Background())
if err == nil || err.Error() != "本地激活服务身份不可信" {
t.Fatal("未拒绝服务身份伪装", err)
}
verified := false
for scanner.Scan() {
verified = verified || scanner.Text() == "IDENTITY_VERIFIED"
}
if err := cmd.Wait(); err != nil || !verified {
t.Fatal("身份模拟服务验证失败", err, stderr.String())
}
})
}