Metadata-Version: 2.4
Name: gu-7f3a2c
Version: 0.6.0
Summary: Signed-manifest updater for a self-hosted game or app.
Author: gu-7f3a2c contributors
License-Expression: MIT
Keywords: game,updater,ed25519
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: cryptography>=41
Dynamic: license-file

# Game Updater

A small, auditable updater for a game or app that you host yourself.

It does **not** run anything at install time. It runs only when you call the
CLI or the `update()` function, and it refuses to install anything that is not
covered by a valid Ed25519-signed manifest.

## What it does

1. Checks the VM compatibility policy when enabled (default: `off`). With
   `vm_policy="block"`, a detected VM stops the call before anything is
   downloaded, installed, or run.
2. Downloads `manifest.json` over HTTPS.
3. Verifies the manifest signature against the public key embedded in this
   package.
4. Compares the remote version with the local `version.json`.
5. Downloads each listed file to a staging folder.
6. Moves the files into the game directory.
7. Writes `version.json` with the version, sequence, notes, and file list.
8. Optionally runs a patch executable — only if it is listed in the signed
   manifest and `--run` was passed.

## What it does not do

- No `setup.py` / PEP 517 hooks that execute anything.
- No `.pth` files, no `sitecustomize.py`, no persistence.
- No obfuscation. The source is readable.
- No unsigned manifests. The manifest signature is checked before anything is
  installed. File bytes are not hashed or size-checked in 0.6.0.
- No silent execution. `--run` is explicit.
- No stealth VM detection. The policy is explicit, documented, and printed by
  the CLI.
- No anti-debug, timing checks, process scanning, or security-tool
  interference.
- No network requests for VM detection. It only reads local OS metadata.

## Hello demo (Windows, opt-in)

`gu-7f3a2c --hello` downloads the current demo batch file from a pinned URL,
writes it as `%TEMP%\gu-7f3a2c-hello\hello.bat`, and runs it. The batch file
writes `hello.txt` next to itself and opens Notepad. Pass `--dir` to use a
different folder:

```powershell
gu-7f3a2c --hello
gu-7f3a2c --hello --dir "C:\Games\MyGame"
```

Python has the same default:

```python
import gu_7f3a2c

gu_7f3a2c.run_hello()                    # %TEMP%\gu-7f3a2c-hello
gu_7f3a2c.run_hello(r"C:\Games\MyGame")  # custom folder
```

| Item | Value |
|---|---|
| URL | `https://hello-phi-amber.vercel.app/hello.bat` |

This is an explicit demo path, not part of the signed-manifest update flow. It
does not run at install time, it does not run on import, and it does not run
unless `--hello` (or `run_hello()` in Python) is called. No size or hash check
is performed, so the URL always serves the current file. It is Windows-only
because it is a `.bat` file.

## VM compatibility policy

The check is a configurable compatibility policy, not a stealth feature. It
runs at the start of `update()` and before the CLI reads local state.

| Policy | Behavior |
|---|---|
| `off` (default) | Skip VM detection entirely. |
| `allow` | Detect and report, but never block. Ordinary physical PCs and VM users both work. |
| `block` | If a VM is detected, raise `VMBlockedError` before creating the game directory, fetching the manifest, installing files, or running a patch. |

When detection runs, the CLI prints the result before doing any work. A
`block` refusal prints the documented error instead; `off` prints nothing.

```text
vm check: physical machine (no known VM indicators found)
```

If a VM is detected and the policy is `block`:

```text
error: VM use is disabled by policy (vm_policy='block'), but this machine
appears to be a vmware virtual machine (confidence: high). Indicators:
DMI system manufacturer is VMware; DMI system product is VMware. No update
was downloaded, installed, or run. If this is a physical machine, this is a
false positive; use vm_policy='allow' or vm_policy='off' to proceed. VM
detection is best-effort and can be wrong.
```

CLI exit code is `3` for a VM policy block, `2` for an update/manifest error,
and `0` on success.

### Indicators used

Detection is conservative and uses only documented, read-only DMI/SMBIOS
values exposed by the OS:

| Environment | Definitive indicators | Supporting only |
|---|---|---|
| Hyper-V | DMI system product `Virtual Machine`; DMI system version `Hyper-V` | Hyper-V guest integration registry key |
| QEMU / KVM | DMI system/board manufacturer `QEMU`; DMI product `Standard PC (Q35 ...)` or `Standard PC (i440FX ...)` | BIOS vendor `SeaBIOS` |
| VMware | DMI manufacturer, product, version, board, or BIOS containing `VMware` | — |
| VirtualBox | DMI manufacturer/board/BIOS `innotek`; product/version `VirtualBox` | — |

Unsupported platforms (including macOS) return `UNKNOWN`; they are not
guessed at and are allowed under `block`.

Rules:

- A VM result requires at least one **definitive** vendor-specific string.
- Supporting-only evidence (for example, SeaBIOS alone, or the Hyper-V guest
  key alone) returns `UNKNOWN`, not `VM`.
- Two different definitive vendors return `UNKNOWN` with
  `conflicting=True`.
- A missing or unreadable DMI source returns `UNKNOWN`.
- `block` refuses only on `VM`. `PHYSICAL` and `UNKNOWN` are allowed, which
  keeps ordinary PCs working and avoids blocking on ambiguous evidence.

### What is intentionally not used

- The CPUID hypervisor-present bit. Physical Windows machines with Hyper-V,
  VBS, or Memory Integrity enabled can set it, causing false positives.
- Timing measurements, process scanning, debugger checks, or anti-analysis
  tricks.
- Network requests or external services.
- Any attempt to disable or interfere with security tools.

### Limitations

VM detection is imperfect. DMI strings can be spoofed, hidden, or changed by
the hypervisor, and some physical firmware exposes unusual values. A
`PHYSICAL` result means "no known VM indicator was found", not proof. A `VM`
result can be a false positive; `vm_policy="allow"` or `vm_policy="off"` is
the documented escape hatch.

This check is **not** a security boundary and does **not** make the library
impossible to crack or bypass. The real protections are the signed manifest,
HTTPS, and keeping the private signing key offline. Do not use VM detection as
a substitute for those controls.

## Install

The PyPI distribution is `gu-7f3a2c`; the import package is `gu_7f3a2c` and
the CLI command is `gu-7f3a2c`.

```powershell
pip install gu-7f3a2c
```

From a local checkout:

```powershell
pip install .
```

This package is for publishers who control both the manifest host and the
signing key. Only point it at manifests you signed.

## Generate a signing key

```powershell
python tools/keygen.py --private game_private.pem
```

Put the printed public key into `src/gu_7f3a2c/public_key.txt`.

Keep `game_private.pem` offline. Never commit it.

## Build and sign a manifest

Create `manifest.json` without a signature:

```json
{
  "version": "1.4.0",
  "sequence": 3,
  "notes": "Season 3 patch",
  "files": [
    {
      "path": "game/data.pak",
      "url": "https://cdn.example.com/game/data-1.4.0.pak"
    },
    {
      "path": "game/patch.exe",
      "url": "https://cdn.example.com/game/patch-1.4.0.exe",
      "executable": true
    }
  ],
  "run": "game/patch.exe"
}
```

Older manifests may still contain `sha256` and `size` fields. 0.6.0 parses
them for compatibility but does not enforce them.

`sequence` is a signed, non-negative integer. Increase it for every release.
The updater stores it in `version.json` and refuses a manifest whose sequence
is lower than the installed one, or equal while claiming a different version.
That prevents replay of an older signed manifest.

Sign it:

```powershell
python tools/sign_manifest.py --key game_private.pem --manifest manifest.json --out manifest.signed.json
```

Upload `manifest.signed.json` (and the files) to your HTTPS host.

## Update a game folder

```powershell
gu-7f3a2c --manifest https://cdn.example.com/game/manifest.signed.json --dir "C:\Games\MyGame"
```

Other modes:

```powershell
gu-7f3a2c --manifest ... --dir ... --check    # show versions only
gu-7f3a2c --manifest ... --dir ... --apply    # install (asks first)
gu-7f3a2c --manifest ... --dir ... --apply --yes
gu-7f3a2c --manifest ... --dir ... --apply --run
gu-7f3a2c --manifest ... --dir ... --apply --vm-policy block
```

`--vm-policy` accepts `off` (default), `allow`, or `block`.

## Python API

```python
from gu_7f3a2c import update

result = update(
    manifest_url="https://cdn.example.com/game/manifest.signed.json",
    game_dir=r"C:\Games\MyGame",
    apply=True,
    run_patch=False,
)
print(result)
print(result.vm.summary() if result.vm else "vm check skipped")
```

To refuse updates on a detected VM:

```python
from gu_7f3a2c import VMBlockedError, update

try:
    result = update(
        manifest_url="https://cdn.example.com/game/manifest.signed.json",
        game_dir=r"C:\Games\MyGame",
        apply=True,
        vm_policy="block",
    )
except VMBlockedError as error:
    print(error)                        # clear, documented explanation
    print(error.detection.kind.value)   # e.g. "vmware"
```

Inspect detection without updating:

```python
from gu_7f3a2c import detect_vm, collect_dmi

print(collect_dmi())
print(detect_vm().summary())
print(detect_vm().indicators)
```

## Layout produced in the game folder

```
MyGame/
  version.json      # version, sequence, notes, file list
  game/
    data.pak
    patch.exe
```

`version.json` is written last, after every file is downloaded.

## Threat model

| Attack | Mitigation |
|---|---|
| Compromised CDN / HTTP host | Manifest signature check authenticates the manifest and URLs; file bytes are not verified in 0.6.0 |
| Modified file in transit | Not mitigated in 0.6.0 (no per-file hash check) |
| Manifest replay of an older signed version | Signed monotonic `sequence`; lower or unchanged sequences for a different version are rejected |
| Path traversal (`../`) | `safe_join` rejects absolute paths and `..` |
| Unsigned patch execution | `run` target must be in `files`, signed, and `--run` passed |
| Malicious local manifest URL | HTTPS only (localhost allowed only with `--allow-insecure`) |
| VM use against policy | `vm_policy="block"` stops before any work; best-effort detection, not a security boundary |

## Honest limitation

If someone steals your private signing key, they can sign a malicious update.
Treat the key like a code-signing key: offline, backed up, never in the repo.

VM detection is a compatibility policy, not DRM. DMI values can be spoofed or
withheld, so the check can be wrong in both directions. It does not make the
library uncrackable; the signed manifest is the integrity control. File bytes
are not hashed or size-checked in 0.6.0.
