Implement machine fingerprint hashing (T-501)

This commit is contained in:
ila
2026-07-20 00:19:13 +08:00
parent 94ea334d1c
commit 8d81276fd9
16 changed files with 465 additions and 9 deletions
@@ -0,0 +1,33 @@
package windows
import (
"errors"
"softbox.local/core/licensing"
)
// ErrMachineFingerprintUnavailable reports that a required identifier could
// not be safely read or normalized. It intentionally contains no source value.
var ErrMachineFingerprintUnavailable = errors.New("machine fingerprint is unavailable")
type machineGUIDReader func() (string, error)
type systemVolumeSerialReader func() (uint32, error)
func machineHashFrom(readMachineGUID machineGUIDReader, readSystemVolumeSerial systemVolumeSerialReader) (string, error) {
if readMachineGUID == nil || readSystemVolumeSerial == nil {
return "", ErrMachineFingerprintUnavailable
}
machineGUID, err := readMachineGUID()
if err != nil {
return "", ErrMachineFingerprintUnavailable
}
systemVolumeSerial, err := readSystemVolumeSerial()
if err != nil {
return "", ErrMachineFingerprintUnavailable
}
machineHash, err := licensing.DeriveMachineHash(machineGUID, systemVolumeSerial)
if err != nil {
return "", ErrMachineFingerprintUnavailable
}
return machineHash, nil
}
@@ -0,0 +1,8 @@
//go:build !windows
package windows
// MachineHash is unavailable outside a Windows target.
func MachineHash() (string, error) {
return "", ErrUnsupported
}
@@ -0,0 +1,67 @@
package windows
import (
"errors"
"strings"
"testing"
"softbox.local/core/licensing"
)
func TestMachineHashFromUsesBothRequiredSources(t *testing.T) {
const machineGUID = "6F9619FF-8B86-D011-B42D-00C04FC964FF"
got, err := machineHashFrom(
func() (string, error) { return machineGUID, nil },
func() (uint32, error) { return 0x1a2b3c4d, nil },
)
if err != nil {
t.Fatalf("machineHashFrom() error = %v", err)
}
want, err := licensing.DeriveMachineHash(machineGUID, 0x1a2b3c4d)
if err != nil {
t.Fatalf("DeriveMachineHash() error = %v", err)
}
if got != want {
t.Fatalf("machineHashFrom() = %q, want %q", got, want)
}
}
func TestMachineHashFromFailsClosedWithoutLeakingSourceValues(t *testing.T) {
const secret = "synthetic-machine-guid-secret"
tests := []struct {
name string
readGUID machineGUIDReader
readSerial systemVolumeSerialReader
}{
{
name: "GUID reader failure",
readGUID: func() (string, error) { return "", errors.New(secret) },
readSerial: func() (uint32, error) { return 1, nil },
},
{
name: "volume reader failure",
readGUID: func() (string, error) { return "6f9619ff-8b86-d011-b42d-00c04fc964ff", nil },
readSerial: func() (uint32, error) { return 0, errors.New(secret) },
},
{
name: "invalid GUID",
readGUID: func() (string, error) { return secret, nil },
readSerial: func() (uint32, error) { return 1, nil },
},
{
name: "missing GUID reader",
readSerial: func() (uint32, error) { return 1, nil },
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
_, err := machineHashFrom(test.readGUID, test.readSerial)
if !errors.Is(err, ErrMachineFingerprintUnavailable) {
t.Fatalf("machineHashFrom() error = %v, want ErrMachineFingerprintUnavailable", err)
}
if strings.Contains(err.Error(), secret) {
t.Fatalf("machineHashFrom() leaked a source value in %q", err)
}
})
}
}
@@ -0,0 +1,58 @@
//go:build windows
package windows
import (
"errors"
"path/filepath"
"golang.org/x/sys/windows"
"golang.org/x/sys/windows/registry"
)
const machineGUIDRegistryPath = `SOFTWARE\Microsoft\Cryptography`
// MachineHash reads the two required Windows identifiers only for the duration
// of this call and returns their derived licensing hash.
func MachineHash() (string, error) {
return machineHashFrom(readMachineGUID, readSystemVolumeSerial)
}
func readMachineGUID() (string, error) {
key, err := registry.OpenKey(registry.LOCAL_MACHINE, machineGUIDRegistryPath, registry.QUERY_VALUE)
if err != nil {
return "", err
}
defer key.Close()
value, _, err := key.GetStringValue("MachineGuid")
if err != nil {
return "", err
}
return value, nil
}
func readSystemVolumeSerial() (uint32, error) {
windowsDirectory, err := windows.GetWindowsDirectory()
if err != nil {
return 0, err
}
volumeName := filepath.VolumeName(windowsDirectory)
if len(volumeName) != 2 || volumeName[1] != ':' || !isASCIIAlpha(volumeName[0]) {
return 0, errors.New("windows directory is not on a drive volume")
}
root, err := windows.UTF16PtrFromString(volumeName + `\`)
if err != nil {
return 0, err
}
var serial uint32
if err := windows.GetVolumeInformation(root, nil, 0, &serial, nil, nil, nil, 0); err != nil {
return 0, err
}
return serial, nil
}
func isASCIIAlpha(character byte) bool {
return character >= 'a' && character <= 'z' || character >= 'A' && character <= 'Z'
}
@@ -13,6 +13,9 @@ import (
)
func TestPlatformStubFailsClosed(t *testing.T) {
if _, err := MachineHash(); !errors.Is(err, ErrUnsupported) {
t.Fatalf("MachineHash() error = %v, want ErrUnsupported", err)
}
platform := New()
if _, err := platform.IsRunning("test-app", "C:/test/App.exe"); !errors.Is(err, ErrUnsupported) {
t.Fatalf("IsRunning() error = %v, want ErrUnsupported", err)