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