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) <noreply@anthropic.com>
This commit is contained in:
@@ -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[:])
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user