Implement machine fingerprint hashing (T-501)
This commit is contained in:
@@ -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) {
|
func TestPlatformStubFailsClosed(t *testing.T) {
|
||||||
|
if _, err := MachineHash(); !errors.Is(err, ErrUnsupported) {
|
||||||
|
t.Fatalf("MachineHash() error = %v, want ErrUnsupported", err)
|
||||||
|
}
|
||||||
platform := New()
|
platform := New()
|
||||||
if _, err := platform.IsRunning("test-app", "C:/test/App.exe"); !errors.Is(err, ErrUnsupported) {
|
if _, err := platform.IsRunning("test-app", "C:/test/App.exe"); !errors.Is(err, ErrUnsupported) {
|
||||||
t.Fatalf("IsRunning() error = %v, want ErrUnsupported", err)
|
t.Fatalf("IsRunning() error = %v, want ErrUnsupported", err)
|
||||||
|
|||||||
@@ -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) {
|
func TestPlatformStubFailsClosed(t *testing.T) {
|
||||||
|
if _, err := MachineHash(); !errors.Is(err, ErrUnsupported) {
|
||||||
|
t.Fatalf("MachineHash() error = %v, want ErrUnsupported", err)
|
||||||
|
}
|
||||||
platform := New()
|
platform := New()
|
||||||
if _, err := platform.IsRunning("test-app", "C:/test/App.exe"); !errors.Is(err, ErrUnsupported) {
|
if _, err := platform.IsRunning("test-app", "C:/test/App.exe"); !errors.Is(err, ErrUnsupported) {
|
||||||
t.Fatalf("IsRunning() error = %v, want ErrUnsupported", err)
|
t.Fatalf("IsRunning() error = %v, want ErrUnsupported", err)
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
// Package licensing provides pure, offline licensing primitives.
|
||||||
|
package licensing
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package licensing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const machineHashDomain = "softbox.machine-hash.v1\x00"
|
||||||
|
|
||||||
|
// ErrInvalidMachineGUID reports a missing or malformed Windows MachineGuid.
|
||||||
|
// It intentionally contains no source value.
|
||||||
|
var ErrInvalidMachineGUID = errors.New("machine GUID is invalid")
|
||||||
|
|
||||||
|
// DeriveMachineHash returns the v1, machine-bound license hash for a normalized
|
||||||
|
// Windows MachineGuid and the Windows directory volume serial number. It never
|
||||||
|
// persists either source identifier.
|
||||||
|
func DeriveMachineHash(machineGUID string, systemVolumeSerial uint32) (string, error) {
|
||||||
|
normalizedGUID, err := normalizeMachineGUID(machineGUID)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
input := machineHashDomain + normalizedGUID + "\x00" + fmt.Sprintf("%08x", systemVolumeSerial)
|
||||||
|
sum := sha256.Sum256([]byte(input))
|
||||||
|
return hex.EncodeToString(sum[:]), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeMachineGUID(value string) (string, error) {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if len(value) != 36 {
|
||||||
|
return "", ErrInvalidMachineGUID
|
||||||
|
}
|
||||||
|
|
||||||
|
for index := 0; index < len(value); index++ {
|
||||||
|
character := value[index]
|
||||||
|
if character > 0x7f {
|
||||||
|
return "", ErrInvalidMachineGUID
|
||||||
|
}
|
||||||
|
if index == 8 || index == 13 || index == 18 || index == 23 {
|
||||||
|
if character != '-' {
|
||||||
|
return "", ErrInvalidMachineGUID
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !isASCIHex(character) {
|
||||||
|
return "", ErrInvalidMachineGUID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.ToLower(value), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isASCIHex(character byte) bool {
|
||||||
|
return character >= '0' && character <= '9' ||
|
||||||
|
character >= 'a' && character <= 'f' ||
|
||||||
|
character >= 'A' && character <= 'F'
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package licensing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDeriveMachineHashCanonicalizesGUID(t *testing.T) {
|
||||||
|
const want = "0922bbbd8c44576d1afa6fe1309615963493cf2e1c4fddda20d2cb3ecb3c1dff"
|
||||||
|
|
||||||
|
got, err := DeriveMachineHash(" 6F9619FF-8B86-D011-B42D-00C04FC964FF\t", 0x1a2b3c4d)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DeriveMachineHash() error = %v", err)
|
||||||
|
}
|
||||||
|
if got != want {
|
||||||
|
t.Fatalf("DeriveMachineHash() = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
if len(got) != 64 || strings.ToLower(got) != got {
|
||||||
|
t.Fatalf("DeriveMachineHash() = %q, want 64 lowercase hexadecimal characters", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeriveMachineHashChangesForVolumeSerial(t *testing.T) {
|
||||||
|
guid := "6f9619ff-8b86-d011-b42d-00c04fc964ff"
|
||||||
|
first, err := DeriveMachineHash(guid, 1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DeriveMachineHash(first) error = %v", err)
|
||||||
|
}
|
||||||
|
second, err := DeriveMachineHash(guid, 2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DeriveMachineHash(second) error = %v", err)
|
||||||
|
}
|
||||||
|
if first == second {
|
||||||
|
t.Fatal("DeriveMachineHash() did not include the volume serial number")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeriveMachineHashRejectsInvalidGUID(t *testing.T) {
|
||||||
|
tests := []string{
|
||||||
|
"",
|
||||||
|
"6f9619ff8b86-d011-b42d-00c04fc964ff",
|
||||||
|
"6f9619ff-8b86-d011-b42d-00c04fc964fg",
|
||||||
|
"6f9619ff-8b86-d011-b42d-00c04fc964f\x00",
|
||||||
|
"6f9619ff-8b86-d011-b42d-00c04fc964fé",
|
||||||
|
}
|
||||||
|
for _, machineGUID := range tests {
|
||||||
|
_, err := DeriveMachineHash(machineGUID, 1)
|
||||||
|
if !errors.Is(err, ErrInvalidMachineGUID) {
|
||||||
|
t.Fatalf("DeriveMachineHash(%q) error = %v, want ErrInvalidMachineGUID", machineGUID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,7 +47,7 @@ SoftBox 软件盒子是一个使用 Go + Gio 开发的 Windows 桌面客户端,
|
|||||||
|
|
||||||
## 当前阶段
|
## 当前阶段
|
||||||
|
|
||||||
当前项目已完成 Phase 0~2、T-301~T-303、T-615 与审核整改 `T-604`~`T-617`。Windows 安全路径阻断项、图标缓存资源边界、后台结果回 UI 线程的事件接线、双适配器交互契约、`VisibleItems` 快照生命周期、双端 Gio shell 职责拆分、unsafe cache 安全诊断/runbook、ZIP 中央目录/EOCD(含 ZIP64)预扫描、安装文件/目录/journal 的代码层耐久顺序、Catalog canonicalization/签名静态 corpus,以及同句柄 Catalog size/SHA→严格 app.json→staging/switch/回滚安装链均已关闭;T-303 已将 verified-package 的 staging 前磁盘/运行状态预检和稳定失败码落实到 core,T-615 已将 ZIP 输入/staging 输出 I/O 分界、平台磁盘满分类和清理失败落实到 core。T-401 已完成完整路径进程检测、受控启动和 Switcher 临界区复查;T-402 已完成子软件更新编排;T-403 已完成受限 SoftBoxUpdater、自身 EXE 健康确认与可恢复切换;T-616 已完成 production cmd 的 Catalog 快照事件投递和明确空态诊断;T-617 已修复 prepared rename+sync 后的自更新恢复不一致并补足无头故障恢复/health flag 测试。T-501 已正式冻结 machine_hash v1:严格 MachineGuid 与 Windows 目录所在卷序列号构成带域分隔的 SHA-256,任一来源不可用即拒绝。物理断电、文件锁与杀毒软件干扰验证保留到 T-601 发布前环境验证。
|
当前项目已完成 Phase 0~2、T-301~T-303、T-615 与审核整改 `T-604`~`T-617`。Windows 安全路径阻断项、图标缓存资源边界、后台结果回 UI 线程的事件接线、双适配器交互契约、`VisibleItems` 快照生命周期、双端 Gio shell 职责拆分、unsafe cache 安全诊断/runbook、ZIP 中央目录/EOCD(含 ZIP64)预扫描、安装文件/目录/journal 的代码层耐久顺序、Catalog canonicalization/签名静态 corpus,以及同句柄 Catalog size/SHA→严格 app.json→staging/switch/回滚安装链均已关闭;T-303 已将 verified-package 的 staging 前磁盘/运行状态预检和稳定失败码落实到 core,T-615 已将 ZIP 输入/staging 输出 I/O 分界、平台磁盘满分类和清理失败落实到 core。T-401 已完成完整路径进程检测、受控启动和 Switcher 临界区复查;T-402 已完成子软件更新编排;T-403 已完成受限 SoftBoxUpdater、自身 EXE 健康确认与可恢复切换;T-616 已完成 production cmd 的 Catalog 快照事件投递和明确空态诊断;T-617 已修复 prepared rename+sync 后的自更新恢复不一致并补足无头故障恢复/health flag 测试。T-501 已完成 machine_hash v1:严格 MachineGuid 与 Windows 目录所在卷序列号构成带域分隔的 SHA-256,任一来源不可用即拒绝且不保存原始标识。物理断电、文件锁与杀毒软件干扰验证保留到 T-601 发布前环境验证。
|
||||||
|
|
||||||
优先路径:
|
优先路径:
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ SoftBox 软件盒子是一个使用 Go + Gio 开发的 Windows 桌面客户端,
|
|||||||
2. 已完成 Phase 1:清单验签、ZIP 安全解压、原子切换回滚原型。
|
2. 已完成 Phase 1:清单验签、ZIP 安全解压、原子切换回滚原型。
|
||||||
3. 已完成 Phase 2 与 T-301:清单/列表/详情/图标缓存 + 可恢复下载队列。
|
3. 已完成 Phase 2 与 T-301:清单/列表/详情/图标缓存 + 可恢复下载队列。
|
||||||
4. 已完成 T-604:modern/Win7 workspace 与 Gio 版本解析彻底隔离。
|
4. 已完成 T-604:modern/Win7 workspace 与 Gio 版本解析彻底隔离。
|
||||||
5. 已完成 T-606~T-615:图标缓存资源边界、UI 线程事件接线、双 Gio 适配器交互契约、`VisibleItems` generation 生命周期、双端 `shell.go` 同 package 镜像职责拆分、unsafe cache 诊断/人工恢复指引、ZIP 中央目录/EOCD 预扫描、安装耐久顺序、Catalog 静态签名向量,以及 staging 输出 I/O 根因与磁盘满诊断;已完成 T-302/T-303:已验签 Catalog 选择与同句柄 size/SHA、严格 app.json、安全 staging/switch/健康与记录写回滚链路,以及 staging 前磁盘/运行状态预检与稳定失败码。T-401 已完成进程检测、受控启动与切换临界区复查;T-402 已完成关闭确认、自然退出等待和更新编排;T-403 已完成 SoftBoxUpdater 的受限 transaction、PID 自然退出、固定健康启动与恢复;T-616 已完成 Catalog 快照启动投递与明确空态诊断;T-617 已完成 prepared rename+sync 的恢复修复、五 phase Recover 与双端 health flag 参数拒绝测试。T-501 已冻结严格双来源的 machine_hash v1,下一步领取并实现该任务。T-601 仍须补真实 Windows 环境的断电/干扰注入。
|
5. 已完成 T-606~T-615:图标缓存资源边界、UI 线程事件接线、双 Gio 适配器交互契约、`VisibleItems` generation 生命周期、双端 `shell.go` 同 package 镜像职责拆分、unsafe cache 诊断/人工恢复指引、ZIP 中央目录/EOCD 预扫描、安装耐久顺序、Catalog 静态签名向量,以及 staging 输出 I/O 根因与磁盘满诊断;已完成 T-302/T-303:已验签 Catalog 选择与同句柄 size/SHA、严格 app.json、安全 staging/switch/健康与记录写回滚链路,以及 staging 前磁盘/运行状态预检与稳定失败码。T-401 已完成进程检测、受控启动与切换临界区复查;T-402 已完成关闭确认、自然退出等待和更新编排;T-403 已完成 SoftBoxUpdater 的受限 transaction、PID 自然退出、固定健康启动与恢复;T-616 已完成 Catalog 快照启动投递与明确空态诊断;T-617 已完成 prepared rename+sync 的恢复修复、五 phase Recover 与双端 health flag 参数拒绝测试;T-501 已完成严格双来源的 machine_hash v1。下一步按路线图正式落成 Phase 5 的 T-502。T-601 仍须补真实 Windows 环境的断电/干扰注入。
|
||||||
|
|
||||||
## 领取任务规则
|
## 领取任务规则
|
||||||
|
|
||||||
|
|||||||
@@ -13,13 +13,14 @@
|
|||||||
## 当前快照
|
## 当前快照
|
||||||
|
|
||||||
- 日期:2026-07-19
|
- 日期:2026-07-19
|
||||||
- 阶段:Phase 2 已完成(T-201~T-204)并由 T-616 补齐启动 Catalog 快照投递/空态诊断;Phase 3 的 T-301 可恢复下载队列、T-302 安装流程整合、T-303 失败处理/磁盘预检查与 T-615 staging 输出 I/O/磁盘满诊断整改已完成;审核整改 T-604~T-614 与 Phase 4 的 T-401 进程检测、受控启动和切换临界区复查、T-402 子软件更新编排、T-403 受限盒子自更新事务及 T-617 恢复状态机/测试整改已完成
|
- 阶段:Phase 2 已完成(T-201~T-204)并由 T-616 补齐启动 Catalog 快照投递/空态诊断;Phase 3 的 T-301 可恢复下载队列、T-302 安装流程整合、T-303 失败处理/磁盘预检查与 T-615 staging 输出 I/O/磁盘满诊断整改已完成;审核整改 T-604~T-617 与 Phase 4 的 T-401 进程检测、受控启动和切换临界区复查、T-402 子软件更新编排、T-403 受限盒子自更新事务及 T-617 恢复状态机/测试整改已完成;Phase 5 的 T-501 严格双来源 machine_hash v1 已完成
|
||||||
- 技术栈:根 Go 1.25 workspace 只纳入 core/app-modern,`app-win7/go.work` 独立纳入 core/app-win7;版本闸门证明 modern Gio v0.10.1 与 win7 Gio v0.6.0 不交叉解析
|
- 技术栈:根 Go 1.25 workspace 只纳入 core/app-modern,`app-win7/go.work` 独立纳入 core/app-win7;版本闸门证明 modern Gio v0.10.1 与 win7 Gio v0.6.0 不交叉解析
|
||||||
- 生产代码:core 已有 Catalog/本地状态/存储、共享 Windows 安全相对路径策略与静态跨实现 canonicalization/Ed25519 vector corpus(拒绝非法 surrogate、`-0` 和非唯一 Base64 signature,大整数保持 token)、安全 ZIP 解压/回滚原型及 T-302/T-303/T-615 安装 use case(`core/application/install.InstallService` 只取已过滤 Catalog entry + architecture,强制注入 disk/storage-failure/target-state checker;`Extractor.ExtractVerifiedFileWithCheck` 在同一普通文件句柄按 size→SHA-256→EOCD/ZIP64→严格 app.json→已规划 payload 的 staging 前预检→安全 staging 的顺序处理,空间要求为 payload+64 MiB,ZIP 输入错误与 staging 创建/write/sync/close 错误分界并保留原始 I/O 链;平台可识别的后者磁盘满返回 `disk_full`,其余输出 I/O 返回稳定 code 且不触发 switch;清理失败可观察,Recover 仅删除已验证 layout 内的残留 staging;每个实际 payload 文件 hash 与受验证 entrypoint/working directory/min_os/requires_admin 写入 installed-app;health 或记录写失败经 Switcher 回滚;更新 current→backup 紧邻前复查精确 entrypoint,明确运行/检测故障保持旧版本并清理 staging),transaction/switch/rollback/recovery 的 journal、rename、清理经统一 fail-closed 耐久栅栏,Windows 使用目录句柄 FlushFileBuffers)、纯 core `application/launch`(只接收 app ID、受控 current/普通 entrypoint/兼容/授权/运行状态/启动器接口全部 fail closed)、双端 Toolhelp 完整映像路径检测/Win7 可用系统版本判断/无参数受控启动与非 Windows fail-closed stub、发布稳定只读 generation 的无 IO 软件列表模型、按 key in-flight + 流式有界读取 + 32 MiB/256-key LRU 的可信图标缓存、图标 Load/Decode 事件发布用例、有界 application event relay,以及默认并发 2 的持久可恢复下载队列;modern/win7 主循环已接 relay/Invalidate,AppShell 已实现搜索/分类/视图、惰性列表、详情右栏、完整图标失败 identity 生命周期与仅 `unsafe_cache` 可见的安全 locator/人工恢复提示,并按 root/header/catalog/detail/style 同 package 镜像职责拆文件
|
- 生产代码:core 已有 Catalog/本地状态/存储、共享 Windows 安全相对路径策略与静态跨实现 canonicalization/Ed25519 vector corpus(拒绝非法 surrogate、`-0` 和非唯一 Base64 signature,大整数保持 token)、安全 ZIP 解压/回滚原型及 T-302/T-303/T-615 安装 use case(`core/application/install.InstallService` 只取已过滤 Catalog entry + architecture,强制注入 disk/storage-failure/target-state checker;`Extractor.ExtractVerifiedFileWithCheck` 在同一普通文件句柄按 size→SHA-256→EOCD/ZIP64→严格 app.json→已规划 payload 的 staging 前预检→安全 staging 的顺序处理,空间要求为 payload+64 MiB,ZIP 输入错误与 staging 创建/write/sync/close 错误分界并保留原始 I/O 链;平台可识别的后者磁盘满返回 `disk_full`,其余输出 I/O 返回稳定 code 且不触发 switch;清理失败可观察,Recover 仅删除已验证 layout 内的残留 staging;每个实际 payload 文件 hash 与受验证 entrypoint/working directory/min_os/requires_admin 写入 installed-app;health 或记录写失败经 Switcher 回滚;更新 current→backup 紧邻前复查精确 entrypoint,明确运行/检测故障保持旧版本并清理 staging),transaction/switch/rollback/recovery 的 journal、rename、清理经统一 fail-closed 耐久栅栏,Windows 使用目录句柄 FlushFileBuffers)、纯 core `application/launch`(只接收 app ID、受控 current/普通 entrypoint/兼容/授权/运行状态/启动器接口全部 fail closed)、双端 Toolhelp 完整映像路径检测/Win7 可用系统版本判断/无参数受控启动与非 Windows fail-closed stub、发布稳定只读 generation 的无 IO 软件列表模型、按 key in-flight + 流式有界读取 + 32 MiB/256-key LRU 的可信图标缓存、图标 Load/Decode 事件发布用例、有界 application event relay,以及默认并发 2 的持久可恢复下载队列;modern/win7 主循环已接 relay/Invalidate,AppShell 已实现搜索/分类/视图、惰性列表、详情右栏、完整图标失败 identity 生命周期与仅 `unsafe_cache` 可见的安全 locator/人工恢复提示,并按 root/header/catalog/detail/style 同 package 镜像职责拆文件
|
||||||
- T-402 更新用例:`core/application/update` 只接收外层可信的 install selection,验证已装版本/旧 entrypoint后,运行中才请求关闭确认并以 1 秒~10 分钟上限等待自然退出,随后委托已有 `InstallService` 的双重运行复查与 rollback;取消、超时、Toolhelp 检测错误和安装错误均保留稳定 code/错误链,不强杀且不改 `data/`、`licenses/`。两端 platform 对齐 `WaitForExit` 契约,Windows 固定短轮询完整路径,非 Windows 返回明确不支持。
|
- T-402 更新用例:`core/application/update` 只接收外层可信的 install selection,验证已装版本/旧 entrypoint后,运行中才请求关闭确认并以 1 秒~10 分钟上限等待自然退出,随后委托已有 `InstallService` 的双重运行复查与 rollback;取消、超时、Toolhelp 检测错误和安装错误均保留稳定 code/错误链,不强杀且不改 `data/`、`licenses/`。两端 platform 对齐 `WaitForExit` 契约,Windows 固定短轮询完整路径,非 Windows 返回明确不支持。
|
||||||
- T-403 自更新:`core/updater` 只接受正 PID、真实 `<root>/app` 与同 root `<root>/staging/<safe-request-id>`,先等待旧 PID 自然退出,随后恢复遗留 journal 或以 `prepared → target_backed_up → staging_activated → launched → committed` 切换;目录 rename、原子 JSON 和目录 durability 均由受限路径与平台同步栅栏保护。助手只启动固定 `<root>/app/SoftBox.exe --softbox-update-health <request-id>`,主程序在 Gio Layout 前从自身 EXE 写最小 health 确认;失败优先恢复旧 app,Windows 锁阻止恢复时保留 backup/journal,不强杀。两端有无 Gio `cmd/softboxupdater`,非 Windows 平台边界 fail closed;没有自更新下载、签名消费、版本选择或 UI 触发器。
|
- T-403 自更新:`core/updater` 只接受正 PID、真实 `<root>/app` 与同 root `<root>/staging/<safe-request-id>`,先等待旧 PID 自然退出,随后恢复遗留 journal 或以 `prepared → target_backed_up → staging_activated → launched → committed` 切换;目录 rename、原子 JSON 和目录 durability 均由受限路径与平台同步栅栏保护。助手只启动固定 `<root>/app/SoftBox.exe --softbox-update-health <request-id>`,主程序在 Gio Layout 前从自身 EXE 写最小 health 确认;失败优先恢复旧 app,Windows 锁阻止恢复时保留 backup/journal,不强杀。两端有无 Gio `cmd/softboxupdater`,非 Windows 平台边界 fail closed;没有自更新下载、签名消费、版本选择或 UI 触发器。
|
||||||
- T-616 启动 Catalog:`core/application.CatalogBootstrap` 只消费 composition 注入的已验证、目标过滤内存快照,以深拷贝 `CatalogRefreshed`/稳定 `CatalogRejected` payload 经现有 runtime/relay 交给 UI Frame;双端 shell 在 UI goroutine 更新 `SetItems` 并明确显示 loading、未配置、加载失败、已加载空目录与筛选空结果。当前 cmd 有意注入 `UnconfiguredCatalogLoader`,故 `dist/SoftBox.exe` 不会显示 fake 软件,而会显示“Catalog 来源尚未配置”;可信 URL/公钥/缓存 composition 仍待发布配置。
|
- T-616 启动 Catalog:`core/application.CatalogBootstrap` 只消费 composition 注入的已验证、目标过滤内存快照,以深拷贝 `CatalogRefreshed`/稳定 `CatalogRejected` payload 经现有 runtime/relay 交给 UI Frame;双端 shell 在 UI goroutine 更新 `SetItems` 并明确显示 loading、未配置、加载失败、已加载空目录与筛选空结果。当前 cmd 有意注入 `UnconfiguredCatalogLoader`,故 `dist/SoftBox.exe` 不会显示 fake 软件,而会显示“Catalog 来源尚未配置”;可信 URL/公钥/缓存 composition 仍待发布配置。
|
||||||
- T-617 自更新整改:`prepared` journal 不再单独决定是否移动;Recover 仅在 target 存在且 managed backup 缺失时删 journal,target 缺失且 backup 存在时先 restore,其余拓扑 fail closed 并保留材料。首次 backup rename 后目录 sync 错误立即走相同受限恢复路径,恢复包装保留 `ErrRecoveryRequired` 和底层 durability 根因。覆盖 prepared/target_backed_up/staging_activated/launched/committed、transaction/rename/sync/cleanup 和 health timeout 阻塞后 Recover;双端 health flag 的缺 ID/多余参数均被拒绝。Windows 真机锁、杀毒和断电仍待 T-601。
|
- T-617 自更新整改:`prepared` journal 不再单独决定是否移动;Recover 仅在 target 存在且 managed backup 缺失时删 journal,target 缺失且 backup 存在时先 restore,其余拓扑 fail closed 并保留材料。首次 backup rename 后目录 sync 错误立即走相同受限恢复路径,恢复包装保留 `ErrRecoveryRequired` 和底层 durability 根因。覆盖 prepared/target_backed_up/staging_activated/launched/committed、transaction/rename/sync/cleanup 和 health timeout 阻塞后 Recover;双端 health flag 的缺 ID/多余参数均被拒绝。Windows 真机锁、杀毒和断电仍待 T-601。
|
||||||
|
- T-501 机器指纹:`core/licensing.DeriveMachineHash` 是 Go 1.20 兼容的纯算法,严格规范化 `MachineGuid` 并按固定域分隔与 Windows 目录所在卷序列号生成 64 字符小写 SHA-256;两端 `platform/windows.MachineHash` 仅在调用期间读取这两个来源,采集/规范化失败只返回无原始值的 sentinel,非 Windows 返回 `ErrUnsupported`。原始 GUID、卷序列号和 MAC 均不进入持久化、事件、UI 或日志;许可证 JSON/验签/导入仍待 T-502。
|
||||||
- 测试:core 覆盖 Catalog 静态 canonicalization/Ed25519 vectors、非法 surrogate/`-0`/Base64 fail-closed、列表快照 generation/零复制、SemVer/12 状态、本地安装记录、Windows dot-space/设备名/Unicode 折叠路径攻击、ZIP destination 包含性与 EOCD/ZIP64 原始包/中央目录/条目数预扫描、T-302/T-303/T-615 同句柄 package size/SHA、严格/有界 app.json、verified payload 预检 hook、容量精确阈值/故障、程序运行/状态故障、稳定安装失败码、staging write/sync/close ENOSPC 与普通输出 I/O 原因保留、CRC 输入分界、清理失败/受控恢复、payload hash 与启动元数据记录、Catalog 选择拒绝、transaction recovery、health/记录写失败回滚、switch 临界区复查、受控启动的旧 metadata/unsafe layout/缺文件/兼容/授权/运行/启动失败、payload/staging tree/journal/rename/rollback/recovery/cleanup 耐久顺序及错误注入、Windows 原生目录 `FlushFileBuffers`、图标并发/取消/读取边界/LRU、真实目录/symlink fail-closed 与 cache→`unsafe_cache` event、relay 背压与关闭、下载并发/暂停/取消/重试/Range/断连/恢复/事件失败与文件身份替换;两个 app 覆盖 Toolhelp snapshot full-path collision/error seam、OS version 判断和非 Windows fail-closed stub,以及 Editor/视图/分类/行/恢复/关闭接线、500 项 viewport、AppID 控件与分类控件生命周期、详情上下文、空状态语义、UI drain 前后、图标失败身份生命周期与 `unsafe_cache` 详情语义;安装恢复矩阵保持通过
|
- 测试:core 覆盖 Catalog 静态 canonicalization/Ed25519 vectors、非法 surrogate/`-0`/Base64 fail-closed、列表快照 generation/零复制、SemVer/12 状态、本地安装记录、Windows dot-space/设备名/Unicode 折叠路径攻击、ZIP destination 包含性与 EOCD/ZIP64 原始包/中央目录/条目数预扫描、T-302/T-303/T-615 同句柄 package size/SHA、严格/有界 app.json、verified payload 预检 hook、容量精确阈值/故障、程序运行/状态故障、稳定安装失败码、staging write/sync/close ENOSPC 与普通输出 I/O 原因保留、CRC 输入分界、清理失败/受控恢复、payload hash 与启动元数据记录、Catalog 选择拒绝、transaction recovery、health/记录写失败回滚、switch 临界区复查、受控启动的旧 metadata/unsafe layout/缺文件/兼容/授权/运行/启动失败、payload/staging tree/journal/rename/rollback/recovery/cleanup 耐久顺序及错误注入、Windows 原生目录 `FlushFileBuffers`、图标并发/取消/读取边界/LRU、真实目录/symlink fail-closed 与 cache→`unsafe_cache` event、relay 背压与关闭、下载并发/暂停/取消/重试/Range/断连/恢复/事件失败与文件身份替换;两个 app 覆盖 Toolhelp snapshot full-path collision/error seam、OS version 判断和非 Windows fail-closed stub,以及 Editor/视图/分类/行/恢复/关闭接线、500 项 viewport、AppID 控件与分类控件生命周期、详情上下文、空状态语义、UI drain 前后、图标失败身份生命周期与 `unsafe_cache` 详情语义;安装恢复矩阵保持通过
|
||||||
- 数据:`schemas/` 已有 manifest/app.json/installed-app.json/download-task.json v1 Schema并注明 Windows 路径运行时权威规则;`testdata/catalog/` 有公开虚构清单样例和 v1 静态 canonicalization/Ed25519 corpus;`testdata/zip/` 与 `testdata/download/` 记录运行时生成的攻击/传输矩阵
|
- 数据:`schemas/` 已有 manifest/app.json/installed-app.json/download-task.json v1 Schema并注明 Windows 路径运行时权威规则;`testdata/catalog/` 有公开虚构清单样例和 v1 静态 canonicalization/Ed25519 corpus;`testdata/zip/` 与 `testdata/download/` 记录运行时生成的攻击/传输矩阵
|
||||||
- 标准启动路径:`./init.sh` / `./init.ps1`(同步依赖、执行完整 Phase 0 闸门、打印双目标构建命令)
|
- 标准启动路径:`./init.sh` / `./init.ps1`(同步依赖、执行完整 Phase 0 闸门、打印双目标构建命令)
|
||||||
@@ -32,7 +33,7 @@
|
|||||||
| 路径 | 状态 | 说明 |
|
| 路径 | 状态 | 说明 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `docs/` | 已有 | harness coding 文档集(本次初始化完成) |
|
| `docs/` | 已有 | harness coding 文档集(本次初始化完成) |
|
||||||
| `docs/tasks/` | 已有 | Phase 0~2、T-301~T-303、T-604~T-617、T-401~T-403 已完成 |
|
| `docs/tasks/` | 已有 | Phase 0~2、T-301~T-303、T-604~T-617、T-401~T-403 与 T-501 已完成 |
|
||||||
| `scripts/` | 已有 | harness 治理、core 边界、Go 版本检查与 Phase 0 双平台验证入口 |
|
| `scripts/` | 已有 | harness 治理、core 边界、Go 版本检查与 Phase 0 双平台验证入口 |
|
||||||
| `core/` | 已建 | Go 1.20 兼容;已有正式 Catalog、本地状态/存储、共享 Windows safepath、列表模型、有界并发图标缓存、图标事件/relay、可恢复下载队列与 Phase 1 安装安全原型 |
|
| `core/` | 已建 | Go 1.20 兼容;已有正式 Catalog、本地状态/存储、共享 Windows safepath、列表模型、有界并发图标缓存、图标事件/relay、可恢复下载队列与 Phase 1 安装安全原型 |
|
||||||
| `app-modern/` | 已建 | Go 1.25.0 + Gio v0.10.1;Modern AppShell 已接入虚拟列表、详情、图标事件 drain/过期拒绝和内存 ImageOp,并拆为五类 shell 职责文件 |
|
| `app-modern/` | 已建 | Go 1.25.0 + Gio v0.10.1;Modern AppShell 已接入虚拟列表、详情、图标事件 drain/过期拒绝和内存 ImageOp,并拆为五类 shell 职责文件 |
|
||||||
@@ -44,8 +45,8 @@
|
|||||||
|
|
||||||
任务状态以 `docs/tasks/` 各任务文件 frontmatter 的 `status` 为准。本节只写项目级摘要:
|
任务状态以 `docs/tasks/` 各任务文件 frontmatter 的 `status` 为准。本节只写项目级摘要:
|
||||||
|
|
||||||
- 已完成:Phase 0 的 `T-001`~`T-004`;Phase 1 的 `T-101`、`T-102`、`T-103`;Phase 2 的 `T-201`~`T-204` 与 `T-616`;Phase 3 的 `T-301`~`T-303` 与 `T-615`;审核整改 `T-604`~`T-617`;Phase 4 的 `T-401`~`T-403`。
|
- 已完成:Phase 0 的 `T-001`~`T-004`;Phase 1 的 `T-101`、`T-102`、`T-103`;Phase 2 的 `T-201`~`T-204` 与 `T-616`;Phase 3 的 `T-301`~`T-303` 与 `T-615`;审核整改 `T-604`~`T-617`;Phase 4 的 `T-401`~`T-403`;Phase 5 的 `T-501`。
|
||||||
- 正在进行:无;`T-501` 已正式落成,冻结为严格 MachineGuid + Windows 系统卷序列号的 machine_hash v1,下一步领取实现。T-601 的物理断电与干扰故障注入仍保留为发布前环境验证。
|
- 正在进行:无;下一步可按路线图正式落成 T-502(离线 Ed25519 许可证验证,依赖 T-501)。T-601 的物理断电与干扰故障注入仍保留为发布前环境验证。
|
||||||
|
|
||||||
## 当前可运行内容
|
## 当前可运行内容
|
||||||
|
|
||||||
|
|||||||
+5
-3
@@ -3,12 +3,12 @@ id: T-501
|
|||||||
title: 机器指纹 machine_hash
|
title: 机器指纹 machine_hash
|
||||||
phase: 5
|
phase: 5
|
||||||
deps: [T-001]
|
deps: [T-001]
|
||||||
status: TODO
|
status: DONE
|
||||||
created: 2026-07-19
|
created: 2026-07-19
|
||||||
issue: null
|
issue: null
|
||||||
context_ref: null
|
context_ref: 94ea334487d7180b4af0fb59c129ff678d2c4ad0
|
||||||
claim_branch: null
|
claim_branch: null
|
||||||
work_branch: null
|
work_branch: agent/codex/T-501
|
||||||
write_paths:
|
write_paths:
|
||||||
- docs/tasks/T-501.md
|
- docs/tasks/T-501.md
|
||||||
- core/licensing/
|
- core/licensing/
|
||||||
@@ -55,3 +55,5 @@ MVP 许可证必须在离线验签前可靠地比较 `machine_hash`,但仓库
|
|||||||
## 执行记录
|
## 执行记录
|
||||||
|
|
||||||
- 2026-07-19:正式落成。冻结 machine_hash v1 的两个必需来源、严格规范化和带域分隔的 SHA-256 输入;拒绝单来源/加权回退,避免后续许可证签发端与双客户端生成不一致的摘要。确认两套现有 `x/sys/windows` 版本均提供 Win7 可用的 `GetWindowsDirectory` 和 `GetVolumeInformation`。
|
- 2026-07-19:正式落成。冻结 machine_hash v1 的两个必需来源、严格规范化和带域分隔的 SHA-256 输入;拒绝单来源/加权回退,避免后续许可证签发端与双客户端生成不一致的摘要。确认两套现有 `x/sys/windows` 版本均提供 Win7 可用的 `GetWindowsDirectory` 和 `GetVolumeInformation`。
|
||||||
|
- 2026-07-19:领取任务,基于 `94ea334487d7180b4af0fb59c129ff678d2c4ad0` 在 `agent/codex/T-501` 执行;先重跑基线,再实现纯 core 算法和双端受限采集边界。
|
||||||
|
- 2026-07-19:完成。新增 Go 1.20 兼容的 `core/licensing.DeriveMachineHash`,以严格 ASCII GUID 规范化、固定域分隔和卷序列号生成小写 SHA-256;无效输入只返回无敏感值 sentinel。双端 `platform/windows.MachineHash` 短暂读取注册表和 Windows 目录所在卷,所有采集/规范化失败均折叠为不泄漏原始值的错误;非 Windows stub 保持 `ErrUnsupported`。局部 core/双端平台测试、完整 core/双端 app 测试和 Windows amd64 构建、`./scripts/verify_phase0.ps1`、上下文与治理校验均通过。
|
||||||
|
|||||||
Reference in New Issue
Block a user