From 35da6be99e291f3a239e50665c496bc32b3770fe Mon Sep 17 00:00:00 2001 From: root Date: Fri, 1 May 2026 19:45:27 +0300 Subject: [PATCH] internal/divert: driver installer with SHA256 verification Extracts embedded WinDivert binaries to %PROGRAMDATA%\Drover\windivert\ on first run; subsequent runs detect matching SHAs and no-op. SHA mismatch after write produces an AV-friendly error message pointing the user at adding the directory to exclusions. ARM64 detected at runtime via runtime.GOARCH and refused gracefully. Co-Authored-By: Claude Opus 4.7 (1M context) --- internal/divert/installer.go | 93 +++++++++++++++++++++++++++++++ internal/divert/installer_test.go | 50 +++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 internal/divert/installer.go create mode 100644 internal/divert/installer_test.go diff --git a/internal/divert/installer.go b/internal/divert/installer.go new file mode 100644 index 0000000..c074a0b --- /dev/null +++ b/internal/divert/installer.go @@ -0,0 +1,93 @@ +//go:build windows + +package divert + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "os" + "path/filepath" + "runtime" + "strings" +) + +// DriverPaths records where the WinDivert binaries landed after install. +type DriverPaths struct { + SysPath string // e.g. C:\ProgramData\Drover\windivert\WinDivert64.sys + DllPath string +} + +// InstallDriver extracts the embedded WinDivert.sys + WinDivert.dll +// into %PROGRAMDATA%\Drover\windivert\ and SHA256-verifies them. +// +// On second and subsequent runs, if the existing files already match +// the embedded SHAs, the function is a no-op and just returns paths. +// +// Errors: +// - ARM64 architecture (WinDivert doesn't support it) +// - %PROGRAMDATA% not set or not writable +// - SHA256 mismatch after write (driver corrupted on disk) +func InstallDriver() (*DriverPaths, error) { + if runtime.GOARCH == "arm64" { + return nil, fmt.Errorf("Drover requires x86-64 Windows; ARM64 is not supported (WinDivert does not ship an ARM64 driver)") + } + pd := os.Getenv("ProgramData") + if pd == "" { + return nil, fmt.Errorf("ProgramData environment variable is not set") + } + dst := filepath.Join(pd, "Drover", "windivert") + return installDriverInto(dst) +} + +func installDriverInto(dst string) (*DriverPaths, error) { + if runtime.GOARCH == "arm64" { + return nil, fmt.Errorf("Drover requires x86-64 Windows; ARM64 is not supported") + } + if err := os.MkdirAll(dst, 0755); err != nil { + return nil, fmt.Errorf("create %s: %w", dst, err) + } + sysPath := filepath.Join(dst, "WinDivert64.sys") + dllPath := filepath.Join(dst, "WinDivert.dll") + + if err := writeIfDifferent(sysPath, winDivertSys, WinDivertSysSHA256); err != nil { + return nil, fmt.Errorf("install WinDivert64.sys: %w", err) + } + if err := writeIfDifferent(dllPath, winDivertDll, WinDivertDllSHA256); err != nil { + return nil, fmt.Errorf("install WinDivert.dll: %w", err) + } + return &DriverPaths{SysPath: sysPath, DllPath: dllPath}, nil +} + +// writeIfDifferent compares the existing file's SHA256 to the expected +// hash; if it matches, no-op. Otherwise overwrite atomically and verify +// the resulting on-disk SHA matches expected. +func writeIfDifferent(path string, content []byte, expectedSHA string) error { + if existing, err := os.ReadFile(path); err == nil { + if strings.EqualFold(sha256Hex(existing), expectedSHA) { + return nil // already up to date + } + } + tmp := path + ".new" + if err := os.WriteFile(tmp, content, 0644); err != nil { + return err + } + if err := os.Rename(tmp, path); err != nil { + _ = os.Remove(tmp) + return err + } + // Verify after write — guards against AV-on-write tampering. + got, err := os.ReadFile(path) + if err != nil { + return err + } + if !strings.EqualFold(sha256Hex(got), expectedSHA) { + return fmt.Errorf("SHA256 mismatch after write at %s; antivirus may have tampered with the file. Add %%PROGRAMDATA%%\\Drover\\ to your AV exclusions and restart Drover", path) + } + return nil +} + +func sha256Hex(b []byte) string { + h := sha256.Sum256(b) + return hex.EncodeToString(h[:]) +} diff --git a/internal/divert/installer_test.go b/internal/divert/installer_test.go new file mode 100644 index 0000000..91a33dd --- /dev/null +++ b/internal/divert/installer_test.go @@ -0,0 +1,50 @@ +package divert + +import ( + "os" + "path/filepath" + "runtime" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestInstallDriver_ExtractsAndVerifies(t *testing.T) { + if runtime.GOOS != "windows" { + t.Skip("Windows-only path") + } + tmp := t.TempDir() + res, err := installDriverInto(tmp) + require.NoError(t, err) + assert.FileExists(t, filepath.Join(tmp, "WinDivert64.sys")) + assert.FileExists(t, filepath.Join(tmp, "WinDivert.dll")) + assert.Equal(t, filepath.Join(tmp, "WinDivert64.sys"), res.SysPath) + assert.Equal(t, filepath.Join(tmp, "WinDivert.dll"), res.DllPath) +} + +func TestInstallDriver_RefusesARM64(t *testing.T) { + if runtime.GOARCH != "arm64" { + t.Skip("only meaningful on arm64") + } + _, err := installDriverInto(t.TempDir()) + require.Error(t, err) + assert.Contains(t, err.Error(), "ARM64") +} + +func TestInstallDriver_DetectsTampering(t *testing.T) { + if runtime.GOOS != "windows" { + t.Skip() + } + tmp := t.TempDir() + // Pre-populate the destination with garbage of the same name so the + // installer's existing-file SHA-check fails and it overwrites. + require.NoError(t, os.WriteFile(filepath.Join(tmp, "WinDivert64.sys"), []byte("garbage"), 0644)) + res, err := installDriverInto(tmp) + require.NoError(t, err) + // After install, the file should have the expected SHA, not garbage. + assert.NotEmpty(t, res.SysPath) + stat, err := os.Stat(res.SysPath) + require.NoError(t, err) + assert.Greater(t, stat.Size(), int64(1000)) +}