game-updater 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- game_updater-0.3.0/LICENSE +21 -0
- game_updater-0.3.0/MANIFEST.in +17 -0
- game_updater-0.3.0/PKG-INFO +289 -0
- game_updater-0.3.0/README.md +276 -0
- game_updater-0.3.0/pyproject.toml +24 -0
- game_updater-0.3.0/setup.cfg +4 -0
- game_updater-0.3.0/src/game_updater/__init__.py +36 -0
- game_updater-0.3.0/src/game_updater/__main__.py +5 -0
- game_updater-0.3.0/src/game_updater/cli.py +146 -0
- game_updater-0.3.0/src/game_updater/download.py +142 -0
- game_updater-0.3.0/src/game_updater/install.py +291 -0
- game_updater-0.3.0/src/game_updater/manifest.py +198 -0
- game_updater-0.3.0/src/game_updater/public_key.txt +1 -0
- game_updater-0.3.0/src/game_updater/vmcheck.py +544 -0
- game_updater-0.3.0/src/game_updater.egg-info/PKG-INFO +289 -0
- game_updater-0.3.0/src/game_updater.egg-info/SOURCES.txt +20 -0
- game_updater-0.3.0/src/game_updater.egg-info/dependency_links.txt +1 -0
- game_updater-0.3.0/src/game_updater.egg-info/entry_points.txt +2 -0
- game_updater-0.3.0/src/game_updater.egg-info/requires.txt +1 -0
- game_updater-0.3.0/src/game_updater.egg-info/top_level.txt +1 -0
- game_updater-0.3.0/tools/keygen.py +66 -0
- game_updater-0.3.0/tools/sign_manifest.py +84 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 game-updater contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
include LICENSE
|
|
2
|
+
include README.md
|
|
3
|
+
include pyproject.toml
|
|
4
|
+
|
|
5
|
+
# The signing and key-generation tools are useful to source users.
|
|
6
|
+
graft tools
|
|
7
|
+
|
|
8
|
+
# Keep test-only payloads out of the published source archive.
|
|
9
|
+
prune tests
|
|
10
|
+
|
|
11
|
+
global-exclude __pycache__
|
|
12
|
+
global-exclude *.py[cod]
|
|
13
|
+
global-exclude *.pyo
|
|
14
|
+
global-exclude *.pem
|
|
15
|
+
global-exclude *.key
|
|
16
|
+
global-exclude *.p12
|
|
17
|
+
global-exclude *.pfx
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: game-updater
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Signed, hash-verified updater for a self-hosted game or app.
|
|
5
|
+
Author: game-updater contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: game,updater,ed25519,sha256
|
|
8
|
+
Requires-Python: >=3.9
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: cryptography>=41
|
|
12
|
+
Dynamic: license-file
|
|
13
|
+
|
|
14
|
+
# Game Updater
|
|
15
|
+
|
|
16
|
+
A small, auditable updater for a game or app that you host yourself.
|
|
17
|
+
|
|
18
|
+
It does **not** run anything at install time. It runs only when you call the
|
|
19
|
+
CLI or the `update()` function, and it refuses to install anything that is not
|
|
20
|
+
covered by a valid Ed25519 signature and a matching SHA-256 hash.
|
|
21
|
+
|
|
22
|
+
## What it does
|
|
23
|
+
|
|
24
|
+
1. Checks the VM compatibility policy when enabled (default: `off`). With
|
|
25
|
+
`vm_policy="block"`, a detected VM stops the call before anything is
|
|
26
|
+
downloaded, installed, or run.
|
|
27
|
+
2. Downloads `manifest.json` over HTTPS.
|
|
28
|
+
3. Verifies the manifest signature against the public key embedded in this
|
|
29
|
+
package.
|
|
30
|
+
4. Compares the remote version with the local `version.json`.
|
|
31
|
+
5. Downloads each listed file to a staging folder.
|
|
32
|
+
6. Verifies size and SHA-256 for every file.
|
|
33
|
+
7. Moves the verified files into the game directory.
|
|
34
|
+
8. Writes `version.json` with the version, sequence, notes, and file list.
|
|
35
|
+
9. Optionally runs a patch executable — only if it is listed in the signed
|
|
36
|
+
manifest, hash-verified, and `--run` was passed.
|
|
37
|
+
|
|
38
|
+
## What it does not do
|
|
39
|
+
|
|
40
|
+
- No `setup.py` / PEP 517 hooks that execute anything.
|
|
41
|
+
- No `.pth` files, no `sitecustomize.py`, no persistence.
|
|
42
|
+
- No obfuscation. The source is readable.
|
|
43
|
+
- No unsigned downloads. Every byte is checked against the signed manifest.
|
|
44
|
+
- No silent execution. `--run` is explicit.
|
|
45
|
+
- No stealth VM detection. The policy is explicit, documented, and printed by
|
|
46
|
+
the CLI.
|
|
47
|
+
- No anti-debug, timing checks, process scanning, or security-tool
|
|
48
|
+
interference.
|
|
49
|
+
- No network requests for VM detection. It only reads local OS metadata.
|
|
50
|
+
|
|
51
|
+
## VM compatibility policy
|
|
52
|
+
|
|
53
|
+
The check is a configurable compatibility policy, not a stealth feature. It
|
|
54
|
+
runs at the start of `update()` and before the CLI reads local state.
|
|
55
|
+
|
|
56
|
+
| Policy | Behavior |
|
|
57
|
+
|---|---|
|
|
58
|
+
| `off` (default) | Skip VM detection entirely. |
|
|
59
|
+
| `allow` | Detect and report, but never block. Ordinary physical PCs and VM users both work. |
|
|
60
|
+
| `block` | If a VM is detected, raise `VMBlockedError` before creating the game directory, fetching the manifest, installing files, or running a patch. |
|
|
61
|
+
|
|
62
|
+
When detection runs, the CLI prints the result before doing any work. A
|
|
63
|
+
`block` refusal prints the documented error instead; `off` prints nothing.
|
|
64
|
+
|
|
65
|
+
```text
|
|
66
|
+
vm check: physical machine (no known VM indicators found)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
If a VM is detected and the policy is `block`:
|
|
70
|
+
|
|
71
|
+
```text
|
|
72
|
+
error: VM use is disabled by policy (vm_policy='block'), but this machine
|
|
73
|
+
appears to be a vmware virtual machine (confidence: high). Indicators:
|
|
74
|
+
DMI system manufacturer is VMware; DMI system product is VMware. No update
|
|
75
|
+
was downloaded, installed, or run. If this is a physical machine, this is a
|
|
76
|
+
false positive; use vm_policy='allow' or vm_policy='off' to proceed. VM
|
|
77
|
+
detection is best-effort and can be wrong.
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
CLI exit code is `3` for a VM policy block, `2` for an update/manifest error,
|
|
81
|
+
and `0` on success.
|
|
82
|
+
|
|
83
|
+
### Indicators used
|
|
84
|
+
|
|
85
|
+
Detection is conservative and uses only documented, read-only DMI/SMBIOS
|
|
86
|
+
values exposed by the OS:
|
|
87
|
+
|
|
88
|
+
| Environment | Definitive indicators | Supporting only |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| Hyper-V | DMI system product `Virtual Machine`; DMI system version `Hyper-V` | Hyper-V guest integration registry key |
|
|
91
|
+
| QEMU / KVM | DMI system/board manufacturer `QEMU`; DMI product `Standard PC (Q35 ...)` or `Standard PC (i440FX ...)` | BIOS vendor `SeaBIOS` |
|
|
92
|
+
| VMware | DMI manufacturer, product, version, board, or BIOS containing `VMware` | — |
|
|
93
|
+
| VirtualBox | DMI manufacturer/board/BIOS `innotek`; product/version `VirtualBox` | — |
|
|
94
|
+
|
|
95
|
+
Unsupported platforms (including macOS) return `UNKNOWN`; they are not
|
|
96
|
+
guessed at and are allowed under `block`.
|
|
97
|
+
|
|
98
|
+
Rules:
|
|
99
|
+
|
|
100
|
+
- A VM result requires at least one **definitive** vendor-specific string.
|
|
101
|
+
- Supporting-only evidence (for example, SeaBIOS alone, or the Hyper-V guest
|
|
102
|
+
key alone) returns `UNKNOWN`, not `VM`.
|
|
103
|
+
- Two different definitive vendors return `UNKNOWN` with
|
|
104
|
+
`conflicting=True`.
|
|
105
|
+
- A missing or unreadable DMI source returns `UNKNOWN`.
|
|
106
|
+
- `block` refuses only on `VM`. `PHYSICAL` and `UNKNOWN` are allowed, which
|
|
107
|
+
keeps ordinary PCs working and avoids blocking on ambiguous evidence.
|
|
108
|
+
|
|
109
|
+
### What is intentionally not used
|
|
110
|
+
|
|
111
|
+
- The CPUID hypervisor-present bit. Physical Windows machines with Hyper-V,
|
|
112
|
+
VBS, or Memory Integrity enabled can set it, causing false positives.
|
|
113
|
+
- Timing measurements, process scanning, debugger checks, or anti-analysis
|
|
114
|
+
tricks.
|
|
115
|
+
- Network requests or external services.
|
|
116
|
+
- Any attempt to disable or interfere with security tools.
|
|
117
|
+
|
|
118
|
+
### Limitations
|
|
119
|
+
|
|
120
|
+
VM detection is imperfect. DMI strings can be spoofed, hidden, or changed by
|
|
121
|
+
the hypervisor, and some physical firmware exposes unusual values. A
|
|
122
|
+
`PHYSICAL` result means "no known VM indicator was found", not proof. A `VM`
|
|
123
|
+
result can be a false positive; `vm_policy="allow"` or `vm_policy="off"` is
|
|
124
|
+
the documented escape hatch.
|
|
125
|
+
|
|
126
|
+
This check is **not** a security boundary and does **not** make the library
|
|
127
|
+
impossible to crack or bypass. The real protections are the signed manifest,
|
|
128
|
+
SHA-256 verification, HTTPS, and keeping the private signing key offline. Do
|
|
129
|
+
not use VM detection as a substitute for those controls.
|
|
130
|
+
|
|
131
|
+
## Install
|
|
132
|
+
|
|
133
|
+
```powershell
|
|
134
|
+
pip install game-updater
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
From a local checkout:
|
|
138
|
+
|
|
139
|
+
```powershell
|
|
140
|
+
pip install .
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
This package is for publishers who control both the manifest host and the
|
|
144
|
+
signing key. Only point it at manifests you signed.
|
|
145
|
+
|
|
146
|
+
## Generate a signing key
|
|
147
|
+
|
|
148
|
+
```powershell
|
|
149
|
+
python tools/keygen.py --private game_private.pem
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Put the printed public key into `src/game_updater/public_key.txt`.
|
|
153
|
+
|
|
154
|
+
Keep `game_private.pem` offline. Never commit it.
|
|
155
|
+
|
|
156
|
+
## Build and sign a manifest
|
|
157
|
+
|
|
158
|
+
Create `manifest.json` without a signature:
|
|
159
|
+
|
|
160
|
+
```json
|
|
161
|
+
{
|
|
162
|
+
"version": "1.4.0",
|
|
163
|
+
"sequence": 3,
|
|
164
|
+
"notes": "Season 3 patch",
|
|
165
|
+
"files": [
|
|
166
|
+
{
|
|
167
|
+
"path": "game/data.pak",
|
|
168
|
+
"url": "https://cdn.example.com/game/data-1.4.0.pak",
|
|
169
|
+
"sha256": "…",
|
|
170
|
+
"size": 12345678
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
"path": "game/patch.exe",
|
|
174
|
+
"url": "https://cdn.example.com/game/patch-1.4.0.exe",
|
|
175
|
+
"sha256": "…",
|
|
176
|
+
"size": 456789,
|
|
177
|
+
"executable": true
|
|
178
|
+
}
|
|
179
|
+
],
|
|
180
|
+
"run": "game/patch.exe"
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
`sequence` is a signed, non-negative integer. Increase it for every release.
|
|
185
|
+
The updater stores it in `version.json` and refuses a manifest whose sequence
|
|
186
|
+
is lower than the installed one, or equal while claiming a different version.
|
|
187
|
+
That prevents replay of an older signed manifest.
|
|
188
|
+
|
|
189
|
+
Sign it:
|
|
190
|
+
|
|
191
|
+
```powershell
|
|
192
|
+
python tools/sign_manifest.py --key game_private.pem --manifest manifest.json --out manifest.signed.json
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Upload `manifest.signed.json` (and the files) to your HTTPS host.
|
|
196
|
+
|
|
197
|
+
## Update a game folder
|
|
198
|
+
|
|
199
|
+
```powershell
|
|
200
|
+
game-updater --manifest https://cdn.example.com/game/manifest.signed.json --dir "C:\Games\MyGame"
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Other modes:
|
|
204
|
+
|
|
205
|
+
```powershell
|
|
206
|
+
game-updater --manifest ... --dir ... --check # show versions only
|
|
207
|
+
game-updater --manifest ... --dir ... --apply # install (asks first)
|
|
208
|
+
game-updater --manifest ... --dir ... --apply --yes
|
|
209
|
+
game-updater --manifest ... --dir ... --apply --run
|
|
210
|
+
game-updater --manifest ... --dir ... --apply --vm-policy block
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
`--vm-policy` accepts `off` (default), `allow`, or `block`.
|
|
214
|
+
|
|
215
|
+
## Python API
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
from game_updater import update
|
|
219
|
+
|
|
220
|
+
result = update(
|
|
221
|
+
manifest_url="https://cdn.example.com/game/manifest.signed.json",
|
|
222
|
+
game_dir=r"C:\Games\MyGame",
|
|
223
|
+
apply=True,
|
|
224
|
+
run_patch=False,
|
|
225
|
+
)
|
|
226
|
+
print(result)
|
|
227
|
+
print(result.vm.summary() if result.vm else "vm check skipped")
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
To refuse updates on a detected VM:
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
from game_updater import VMBlockedError, update
|
|
234
|
+
|
|
235
|
+
try:
|
|
236
|
+
result = update(
|
|
237
|
+
manifest_url="https://cdn.example.com/game/manifest.signed.json",
|
|
238
|
+
game_dir=r"C:\Games\MyGame",
|
|
239
|
+
apply=True,
|
|
240
|
+
vm_policy="block",
|
|
241
|
+
)
|
|
242
|
+
except VMBlockedError as error:
|
|
243
|
+
print(error) # clear, documented explanation
|
|
244
|
+
print(error.detection.kind.value) # e.g. "vmware"
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Inspect detection without updating:
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
from game_updater import detect_vm, collect_dmi
|
|
251
|
+
|
|
252
|
+
print(collect_dmi())
|
|
253
|
+
print(detect_vm().summary())
|
|
254
|
+
print(detect_vm().indicators)
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## Layout produced in the game folder
|
|
258
|
+
|
|
259
|
+
```
|
|
260
|
+
MyGame/
|
|
261
|
+
version.json # version, sequence, notes, file list
|
|
262
|
+
game/
|
|
263
|
+
data.pak
|
|
264
|
+
patch.exe
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
`version.json` is written last, after every file is verified.
|
|
268
|
+
|
|
269
|
+
## Threat model
|
|
270
|
+
|
|
271
|
+
| Attack | Mitigation |
|
|
272
|
+
|---|---|
|
|
273
|
+
| Compromised CDN / HTTP host | Manifest signature check |
|
|
274
|
+
| Modified file in transit | SHA-256 per file |
|
|
275
|
+
| Manifest replay of an older signed version | Signed monotonic `sequence`; lower or unchanged sequences for a different version are rejected |
|
|
276
|
+
| Path traversal (`../`) | `safe_join` rejects absolute paths and `..` |
|
|
277
|
+
| Unsigned patch execution | `run` target must be in `files`, verified, and `--run` passed |
|
|
278
|
+
| Malicious local manifest URL | HTTPS only (localhost allowed only with `--allow-insecure`) |
|
|
279
|
+
| VM use against policy | `vm_policy="block"` stops before any work; best-effort detection, not a security boundary |
|
|
280
|
+
|
|
281
|
+
## Honest limitation
|
|
282
|
+
|
|
283
|
+
If someone steals your private signing key, they can sign a malicious update.
|
|
284
|
+
Treat the key like a code-signing key: offline, backed up, never in the repo.
|
|
285
|
+
|
|
286
|
+
VM detection is a compatibility policy, not DRM. DMI values can be spoofed or
|
|
287
|
+
withheld, so the check can be wrong in both directions. It does not make the
|
|
288
|
+
library uncrackable; the signed manifest and SHA-256 checks are the actual
|
|
289
|
+
integrity controls.
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# Game Updater
|
|
2
|
+
|
|
3
|
+
A small, auditable updater for a game or app that you host yourself.
|
|
4
|
+
|
|
5
|
+
It does **not** run anything at install time. It runs only when you call the
|
|
6
|
+
CLI or the `update()` function, and it refuses to install anything that is not
|
|
7
|
+
covered by a valid Ed25519 signature and a matching SHA-256 hash.
|
|
8
|
+
|
|
9
|
+
## What it does
|
|
10
|
+
|
|
11
|
+
1. Checks the VM compatibility policy when enabled (default: `off`). With
|
|
12
|
+
`vm_policy="block"`, a detected VM stops the call before anything is
|
|
13
|
+
downloaded, installed, or run.
|
|
14
|
+
2. Downloads `manifest.json` over HTTPS.
|
|
15
|
+
3. Verifies the manifest signature against the public key embedded in this
|
|
16
|
+
package.
|
|
17
|
+
4. Compares the remote version with the local `version.json`.
|
|
18
|
+
5. Downloads each listed file to a staging folder.
|
|
19
|
+
6. Verifies size and SHA-256 for every file.
|
|
20
|
+
7. Moves the verified files into the game directory.
|
|
21
|
+
8. Writes `version.json` with the version, sequence, notes, and file list.
|
|
22
|
+
9. Optionally runs a patch executable — only if it is listed in the signed
|
|
23
|
+
manifest, hash-verified, and `--run` was passed.
|
|
24
|
+
|
|
25
|
+
## What it does not do
|
|
26
|
+
|
|
27
|
+
- No `setup.py` / PEP 517 hooks that execute anything.
|
|
28
|
+
- No `.pth` files, no `sitecustomize.py`, no persistence.
|
|
29
|
+
- No obfuscation. The source is readable.
|
|
30
|
+
- No unsigned downloads. Every byte is checked against the signed manifest.
|
|
31
|
+
- No silent execution. `--run` is explicit.
|
|
32
|
+
- No stealth VM detection. The policy is explicit, documented, and printed by
|
|
33
|
+
the CLI.
|
|
34
|
+
- No anti-debug, timing checks, process scanning, or security-tool
|
|
35
|
+
interference.
|
|
36
|
+
- No network requests for VM detection. It only reads local OS metadata.
|
|
37
|
+
|
|
38
|
+
## VM compatibility policy
|
|
39
|
+
|
|
40
|
+
The check is a configurable compatibility policy, not a stealth feature. It
|
|
41
|
+
runs at the start of `update()` and before the CLI reads local state.
|
|
42
|
+
|
|
43
|
+
| Policy | Behavior |
|
|
44
|
+
|---|---|
|
|
45
|
+
| `off` (default) | Skip VM detection entirely. |
|
|
46
|
+
| `allow` | Detect and report, but never block. Ordinary physical PCs and VM users both work. |
|
|
47
|
+
| `block` | If a VM is detected, raise `VMBlockedError` before creating the game directory, fetching the manifest, installing files, or running a patch. |
|
|
48
|
+
|
|
49
|
+
When detection runs, the CLI prints the result before doing any work. A
|
|
50
|
+
`block` refusal prints the documented error instead; `off` prints nothing.
|
|
51
|
+
|
|
52
|
+
```text
|
|
53
|
+
vm check: physical machine (no known VM indicators found)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
If a VM is detected and the policy is `block`:
|
|
57
|
+
|
|
58
|
+
```text
|
|
59
|
+
error: VM use is disabled by policy (vm_policy='block'), but this machine
|
|
60
|
+
appears to be a vmware virtual machine (confidence: high). Indicators:
|
|
61
|
+
DMI system manufacturer is VMware; DMI system product is VMware. No update
|
|
62
|
+
was downloaded, installed, or run. If this is a physical machine, this is a
|
|
63
|
+
false positive; use vm_policy='allow' or vm_policy='off' to proceed. VM
|
|
64
|
+
detection is best-effort and can be wrong.
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
CLI exit code is `3` for a VM policy block, `2` for an update/manifest error,
|
|
68
|
+
and `0` on success.
|
|
69
|
+
|
|
70
|
+
### Indicators used
|
|
71
|
+
|
|
72
|
+
Detection is conservative and uses only documented, read-only DMI/SMBIOS
|
|
73
|
+
values exposed by the OS:
|
|
74
|
+
|
|
75
|
+
| Environment | Definitive indicators | Supporting only |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| Hyper-V | DMI system product `Virtual Machine`; DMI system version `Hyper-V` | Hyper-V guest integration registry key |
|
|
78
|
+
| QEMU / KVM | DMI system/board manufacturer `QEMU`; DMI product `Standard PC (Q35 ...)` or `Standard PC (i440FX ...)` | BIOS vendor `SeaBIOS` |
|
|
79
|
+
| VMware | DMI manufacturer, product, version, board, or BIOS containing `VMware` | — |
|
|
80
|
+
| VirtualBox | DMI manufacturer/board/BIOS `innotek`; product/version `VirtualBox` | — |
|
|
81
|
+
|
|
82
|
+
Unsupported platforms (including macOS) return `UNKNOWN`; they are not
|
|
83
|
+
guessed at and are allowed under `block`.
|
|
84
|
+
|
|
85
|
+
Rules:
|
|
86
|
+
|
|
87
|
+
- A VM result requires at least one **definitive** vendor-specific string.
|
|
88
|
+
- Supporting-only evidence (for example, SeaBIOS alone, or the Hyper-V guest
|
|
89
|
+
key alone) returns `UNKNOWN`, not `VM`.
|
|
90
|
+
- Two different definitive vendors return `UNKNOWN` with
|
|
91
|
+
`conflicting=True`.
|
|
92
|
+
- A missing or unreadable DMI source returns `UNKNOWN`.
|
|
93
|
+
- `block` refuses only on `VM`. `PHYSICAL` and `UNKNOWN` are allowed, which
|
|
94
|
+
keeps ordinary PCs working and avoids blocking on ambiguous evidence.
|
|
95
|
+
|
|
96
|
+
### What is intentionally not used
|
|
97
|
+
|
|
98
|
+
- The CPUID hypervisor-present bit. Physical Windows machines with Hyper-V,
|
|
99
|
+
VBS, or Memory Integrity enabled can set it, causing false positives.
|
|
100
|
+
- Timing measurements, process scanning, debugger checks, or anti-analysis
|
|
101
|
+
tricks.
|
|
102
|
+
- Network requests or external services.
|
|
103
|
+
- Any attempt to disable or interfere with security tools.
|
|
104
|
+
|
|
105
|
+
### Limitations
|
|
106
|
+
|
|
107
|
+
VM detection is imperfect. DMI strings can be spoofed, hidden, or changed by
|
|
108
|
+
the hypervisor, and some physical firmware exposes unusual values. A
|
|
109
|
+
`PHYSICAL` result means "no known VM indicator was found", not proof. A `VM`
|
|
110
|
+
result can be a false positive; `vm_policy="allow"` or `vm_policy="off"` is
|
|
111
|
+
the documented escape hatch.
|
|
112
|
+
|
|
113
|
+
This check is **not** a security boundary and does **not** make the library
|
|
114
|
+
impossible to crack or bypass. The real protections are the signed manifest,
|
|
115
|
+
SHA-256 verification, HTTPS, and keeping the private signing key offline. Do
|
|
116
|
+
not use VM detection as a substitute for those controls.
|
|
117
|
+
|
|
118
|
+
## Install
|
|
119
|
+
|
|
120
|
+
```powershell
|
|
121
|
+
pip install game-updater
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
From a local checkout:
|
|
125
|
+
|
|
126
|
+
```powershell
|
|
127
|
+
pip install .
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
This package is for publishers who control both the manifest host and the
|
|
131
|
+
signing key. Only point it at manifests you signed.
|
|
132
|
+
|
|
133
|
+
## Generate a signing key
|
|
134
|
+
|
|
135
|
+
```powershell
|
|
136
|
+
python tools/keygen.py --private game_private.pem
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Put the printed public key into `src/game_updater/public_key.txt`.
|
|
140
|
+
|
|
141
|
+
Keep `game_private.pem` offline. Never commit it.
|
|
142
|
+
|
|
143
|
+
## Build and sign a manifest
|
|
144
|
+
|
|
145
|
+
Create `manifest.json` without a signature:
|
|
146
|
+
|
|
147
|
+
```json
|
|
148
|
+
{
|
|
149
|
+
"version": "1.4.0",
|
|
150
|
+
"sequence": 3,
|
|
151
|
+
"notes": "Season 3 patch",
|
|
152
|
+
"files": [
|
|
153
|
+
{
|
|
154
|
+
"path": "game/data.pak",
|
|
155
|
+
"url": "https://cdn.example.com/game/data-1.4.0.pak",
|
|
156
|
+
"sha256": "…",
|
|
157
|
+
"size": 12345678
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
"path": "game/patch.exe",
|
|
161
|
+
"url": "https://cdn.example.com/game/patch-1.4.0.exe",
|
|
162
|
+
"sha256": "…",
|
|
163
|
+
"size": 456789,
|
|
164
|
+
"executable": true
|
|
165
|
+
}
|
|
166
|
+
],
|
|
167
|
+
"run": "game/patch.exe"
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
`sequence` is a signed, non-negative integer. Increase it for every release.
|
|
172
|
+
The updater stores it in `version.json` and refuses a manifest whose sequence
|
|
173
|
+
is lower than the installed one, or equal while claiming a different version.
|
|
174
|
+
That prevents replay of an older signed manifest.
|
|
175
|
+
|
|
176
|
+
Sign it:
|
|
177
|
+
|
|
178
|
+
```powershell
|
|
179
|
+
python tools/sign_manifest.py --key game_private.pem --manifest manifest.json --out manifest.signed.json
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Upload `manifest.signed.json` (and the files) to your HTTPS host.
|
|
183
|
+
|
|
184
|
+
## Update a game folder
|
|
185
|
+
|
|
186
|
+
```powershell
|
|
187
|
+
game-updater --manifest https://cdn.example.com/game/manifest.signed.json --dir "C:\Games\MyGame"
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Other modes:
|
|
191
|
+
|
|
192
|
+
```powershell
|
|
193
|
+
game-updater --manifest ... --dir ... --check # show versions only
|
|
194
|
+
game-updater --manifest ... --dir ... --apply # install (asks first)
|
|
195
|
+
game-updater --manifest ... --dir ... --apply --yes
|
|
196
|
+
game-updater --manifest ... --dir ... --apply --run
|
|
197
|
+
game-updater --manifest ... --dir ... --apply --vm-policy block
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
`--vm-policy` accepts `off` (default), `allow`, or `block`.
|
|
201
|
+
|
|
202
|
+
## Python API
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
from game_updater import update
|
|
206
|
+
|
|
207
|
+
result = update(
|
|
208
|
+
manifest_url="https://cdn.example.com/game/manifest.signed.json",
|
|
209
|
+
game_dir=r"C:\Games\MyGame",
|
|
210
|
+
apply=True,
|
|
211
|
+
run_patch=False,
|
|
212
|
+
)
|
|
213
|
+
print(result)
|
|
214
|
+
print(result.vm.summary() if result.vm else "vm check skipped")
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
To refuse updates on a detected VM:
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
from game_updater import VMBlockedError, update
|
|
221
|
+
|
|
222
|
+
try:
|
|
223
|
+
result = update(
|
|
224
|
+
manifest_url="https://cdn.example.com/game/manifest.signed.json",
|
|
225
|
+
game_dir=r"C:\Games\MyGame",
|
|
226
|
+
apply=True,
|
|
227
|
+
vm_policy="block",
|
|
228
|
+
)
|
|
229
|
+
except VMBlockedError as error:
|
|
230
|
+
print(error) # clear, documented explanation
|
|
231
|
+
print(error.detection.kind.value) # e.g. "vmware"
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Inspect detection without updating:
|
|
235
|
+
|
|
236
|
+
```python
|
|
237
|
+
from game_updater import detect_vm, collect_dmi
|
|
238
|
+
|
|
239
|
+
print(collect_dmi())
|
|
240
|
+
print(detect_vm().summary())
|
|
241
|
+
print(detect_vm().indicators)
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## Layout produced in the game folder
|
|
245
|
+
|
|
246
|
+
```
|
|
247
|
+
MyGame/
|
|
248
|
+
version.json # version, sequence, notes, file list
|
|
249
|
+
game/
|
|
250
|
+
data.pak
|
|
251
|
+
patch.exe
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
`version.json` is written last, after every file is verified.
|
|
255
|
+
|
|
256
|
+
## Threat model
|
|
257
|
+
|
|
258
|
+
| Attack | Mitigation |
|
|
259
|
+
|---|---|
|
|
260
|
+
| Compromised CDN / HTTP host | Manifest signature check |
|
|
261
|
+
| Modified file in transit | SHA-256 per file |
|
|
262
|
+
| Manifest replay of an older signed version | Signed monotonic `sequence`; lower or unchanged sequences for a different version are rejected |
|
|
263
|
+
| Path traversal (`../`) | `safe_join` rejects absolute paths and `..` |
|
|
264
|
+
| Unsigned patch execution | `run` target must be in `files`, verified, and `--run` passed |
|
|
265
|
+
| Malicious local manifest URL | HTTPS only (localhost allowed only with `--allow-insecure`) |
|
|
266
|
+
| VM use against policy | `vm_policy="block"` stops before any work; best-effort detection, not a security boundary |
|
|
267
|
+
|
|
268
|
+
## Honest limitation
|
|
269
|
+
|
|
270
|
+
If someone steals your private signing key, they can sign a malicious update.
|
|
271
|
+
Treat the key like a code-signing key: offline, backed up, never in the repo.
|
|
272
|
+
|
|
273
|
+
VM detection is a compatibility policy, not DRM. DMI values can be spoofed or
|
|
274
|
+
withheld, so the check can be wrong in both directions. It does not make the
|
|
275
|
+
library uncrackable; the signed manifest and SHA-256 checks are the actual
|
|
276
|
+
integrity controls.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "game-updater"
|
|
7
|
+
version = "0.3.0"
|
|
8
|
+
description = "Signed, hash-verified updater for a self-hosted game or app."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "game-updater contributors" }]
|
|
14
|
+
keywords = ["game", "updater", "ed25519", "sha256"]
|
|
15
|
+
dependencies = ["cryptography>=41"]
|
|
16
|
+
|
|
17
|
+
[project.scripts]
|
|
18
|
+
game-updater = "game_updater.cli:main"
|
|
19
|
+
|
|
20
|
+
[tool.setuptools.packages.find]
|
|
21
|
+
where = ["src"]
|
|
22
|
+
|
|
23
|
+
[tool.setuptools.package-data]
|
|
24
|
+
game_updater = ["public_key.txt"]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Signed, hash-verified updater for a self-hosted game or app."""
|
|
2
|
+
|
|
3
|
+
from .install import UpdateResult, update
|
|
4
|
+
from .manifest import Manifest, ManifestError
|
|
5
|
+
from .vmcheck import (
|
|
6
|
+
DMIData,
|
|
7
|
+
VMBlockedError,
|
|
8
|
+
VMDetection,
|
|
9
|
+
VMPolicy,
|
|
10
|
+
VMStatus,
|
|
11
|
+
VMVendor,
|
|
12
|
+
classify,
|
|
13
|
+
collect_dmi,
|
|
14
|
+
detect_vm,
|
|
15
|
+
ensure_vm_allowed,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__version__ = "0.3.0"
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"__version__",
|
|
22
|
+
"DMIData",
|
|
23
|
+
"Manifest",
|
|
24
|
+
"ManifestError",
|
|
25
|
+
"UpdateResult",
|
|
26
|
+
"VMBlockedError",
|
|
27
|
+
"VMDetection",
|
|
28
|
+
"VMPolicy",
|
|
29
|
+
"VMStatus",
|
|
30
|
+
"VMVendor",
|
|
31
|
+
"classify",
|
|
32
|
+
"collect_dmi",
|
|
33
|
+
"detect_vm",
|
|
34
|
+
"ensure_vm_allowed",
|
|
35
|
+
"update",
|
|
36
|
+
]
|