docs: document m2pack launcher integration
adds docs/m2pack-integration.md covering the signature boundary, runtime key env-var delivery, telemetry opt-in, backward compatibility and expected on-disk layout. README gains a short "Release formats" section pointing at the new doc, and CHANGELOG tracks the [Unreleased] entries. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
49
CHANGELOG.md
Normal file
49
CHANGELOG.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to `metin-launcher` are tracked here. Format loosely
|
||||
follows Keep a Changelog; dates are Europe/Prague.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- Release format dispatch. The manifest now carries an optional top-level
|
||||
`format` field (defaults to `legacy-json-blob`) and the orchestrator
|
||||
resolves an `IReleaseFormat` strategy via `ReleaseFormatFactory`.
|
||||
- `M2PackFormat`: new release format that lists `.m2p` pack archives plus a
|
||||
`runtime-key.json` sidecar. The launcher never opens or decrypts `.m2p`
|
||||
archives; it only places them next to the client root and loads the
|
||||
runtime key after apply.
|
||||
- `RuntimeKey` model + `IRuntimeKeyDelivery` strategy. `EnvVarKeyDelivery`
|
||||
is the MVP implementation and forwards `M2PACK_MASTER_KEY_HEX`,
|
||||
`M2PACK_SIGN_PUBKEY_HEX` and `M2PACK_KEY_ID` to the child process scoped
|
||||
to `ProcessStartInfo.Environment` only. `SharedMemoryKeyDelivery` is
|
||||
documented as a stub and throws until the Windows receiver lands.
|
||||
- `GameProcess.BuildStartInfo` now accepts an optional `RuntimeKey?` and
|
||||
forwards it through the env-var delivery.
|
||||
- `ClientAppliedReporter`: opt-in best-effort telemetry ping that fires
|
||||
once after a successful apply with a 5-second cap. Disabled by default
|
||||
(`LauncherConfig.TelemetryUrlTemplate == ""`). Failures are always
|
||||
swallowed and logged as warnings.
|
||||
- `docs/m2pack-integration.md` documenting the signature boundary, runtime
|
||||
key delivery, telemetry, backward compatibility and file layout.
|
||||
- ~60 new tests across `RuntimeKeyTests`, `EnvVarDeliveryTests`,
|
||||
`LegacyJsonBlobFormatTests`, `M2PackFormatTests`, `ReleaseFormatFactoryTests`,
|
||||
`ClientAppliedReporterTests`, `UpdateOrchestratorFormatDispatchTests` and
|
||||
extended `GameProcessTests`. Total suite is ~92 tests.
|
||||
|
||||
### Changed
|
||||
- `UpdateOrchestrator` dispatches through `ReleaseFormatFactory` after
|
||||
signature verification. The legacy flow is preserved byte-for-byte for
|
||||
manifests without a `format` field; the only visible difference is that
|
||||
the complete log line now reads `format=legacy-json-blob`.
|
||||
- `UpdateOrchestrator.Result` gained `Format` and `RuntimeKey` slots so the
|
||||
headless entry point can forward the runtime key into `GameProcess.Launch`.
|
||||
|
||||
### Security
|
||||
- A signed manifest carrying an unknown `format` value is refused outright
|
||||
rather than silently falling back to legacy, preventing a downgrade
|
||||
attack vector in the event of a signing key compromise.
|
||||
- Env vars produced by `EnvVarKeyDelivery` are scoped to the spawned
|
||||
child's environment only. The launcher never mutates its own process
|
||||
environment, so other processes on the machine and later code in the
|
||||
launcher itself cannot read the key.
|
||||
24
README.md
24
README.md
@@ -125,6 +125,30 @@ tests/Metin2Launcher.Tests/
|
||||
game assets (~4 GB) are handled by our own patcher code.
|
||||
- **xunit / xunit.runner.visualstudio** — tests.
|
||||
|
||||
## Release formats
|
||||
|
||||
The launcher dispatches on a top-level `format` field in the signed manifest.
|
||||
Two strategies exist today:
|
||||
|
||||
- `legacy-json-blob` (default when `format` is absent) — individual files
|
||||
listed in `files[]`, downloaded by sha256 and atomically replaced inside
|
||||
the client root.
|
||||
- `m2pack` — the `files[]` entries are `.m2p` pack archives plus a
|
||||
`runtime-key.json` sidecar. The launcher **never** opens or decrypts
|
||||
`.m2p` archives; it just places them next to the client root and forwards
|
||||
the runtime key to `Metin2.exe` via environment variables
|
||||
(`M2PACK_MASTER_KEY_HEX`, `M2PACK_SIGN_PUBKEY_HEX`, `M2PACK_KEY_ID`).
|
||||
|
||||
A manifest whose `format` value is signed but unknown is refused outright —
|
||||
the orchestrator falls back to offline mode rather than silently downgrading.
|
||||
See `docs/m2pack-integration.md` for the full threat model and file layout.
|
||||
|
||||
An opt-in "client applied this release" telemetry ping is available via
|
||||
`LauncherConfig.TelemetryUrlTemplate`. It is empty by default, which short-
|
||||
circuits the reporter so no network call is made. When set, the reporter
|
||||
fires a single bounded (5s) best-effort POST after a successful apply and
|
||||
swallows any failure.
|
||||
|
||||
## Signing key
|
||||
|
||||
`LauncherConfig.PublicKeyHex` is the Ed25519 public key that every manifest
|
||||
|
||||
114
docs/m2pack-integration.md
Normal file
114
docs/m2pack-integration.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# m2pack integration
|
||||
|
||||
Notes on how `metin-launcher` handles the m2pack release format. Written when the
|
||||
launcher-side support landed on `claude/m2pack-launcher`; the client-side receiver
|
||||
for the runtime key is tracked separately.
|
||||
|
||||
## Scope
|
||||
|
||||
The launcher's job in an m2pack release is narrowly defined:
|
||||
|
||||
1. Fetch the signed `manifest.json` and its `.sig` exactly as before.
|
||||
2. Verify Ed25519 signature against the compiled-in public key, exactly as before.
|
||||
3. Read the new top-level `format` field (falling back to `legacy-json-blob`).
|
||||
4. Dispatch through `ReleaseFormatFactory.Resolve(...)` to an `IReleaseFormat`.
|
||||
5. Diff, download and atomically apply the file entries listed in the manifest.
|
||||
For m2pack these are `.m2p` pack archives plus a `runtime-key.json` sidecar.
|
||||
6. **Never** open, decrypt or decompress a `.m2p` archive. Integrity of the
|
||||
archive contents is the client's problem, gated on the runtime key.
|
||||
7. Load `runtime-key.json` from the client root and forward it to `Metin2.exe`
|
||||
via environment variables (`M2PACK_MASTER_KEY_HEX`, `M2PACK_SIGN_PUBKEY_HEX`,
|
||||
`M2PACK_KEY_ID`), scoped to the child process only.
|
||||
|
||||
Anything beyond step 7 — parsing the archive, selecting pack files, decrypting
|
||||
blocks, validating per-file signatures — lives in the client. The launcher is
|
||||
deliberately dumb about `.m2p`.
|
||||
|
||||
## Signature boundary
|
||||
|
||||
The Ed25519 signature stays on the top-level `manifest.json`. `.m2p` files are
|
||||
each a single `files[]` entry with a `sha256`, exactly like any other blob —
|
||||
that's enough to detect tampering or incomplete downloads, and tamper-resistance
|
||||
of the content-addressed URL is inherited from the signed manifest.
|
||||
|
||||
The `runtime-key.json` sidecar is listed in the manifest in the same way, so
|
||||
its integrity is also covered by the manifest signature. We do **not** sign the
|
||||
runtime key independently.
|
||||
|
||||
## Runtime key delivery
|
||||
|
||||
Two delivery strategies are defined behind `IRuntimeKeyDelivery`:
|
||||
|
||||
- `EnvVarKeyDelivery` — MVP, used on every platform. Sets
|
||||
`M2PACK_MASTER_KEY_HEX`, `M2PACK_SIGN_PUBKEY_HEX` and `M2PACK_KEY_ID` on the
|
||||
child `ProcessStartInfo.Environment`. The launcher never sets these on its
|
||||
own process environment, so unrelated processes can't snoop them and the
|
||||
values don't leak via `Environment.GetEnvironmentVariable` calls elsewhere
|
||||
in the launcher.
|
||||
- `SharedMemoryKeyDelivery` — Windows stub. Not implemented. See the class
|
||||
docs for the planned approach (named `MemoryMappedFile` with a random
|
||||
suffix, write-then-zero lifecycle, child reads via mapping name passed in
|
||||
env). Left as a stub because the client-side receiver hasn't landed.
|
||||
|
||||
Threat model note: env vars are readable by any process that shares the
|
||||
parent's user on Linux (`/proc/$pid/environ`, gated by `ptrace_scope` and by
|
||||
file mode). That's identical to the current legacy flow in terms of secret
|
||||
handling — the legacy release also trusts the local user — and the shared
|
||||
memory strategy is planned for a future hardening pass, not as an MVP
|
||||
requirement.
|
||||
|
||||
## Telemetry
|
||||
|
||||
`ClientAppliedReporter` is wired but disabled by default: `LauncherConfig.TelemetryUrlTemplate`
|
||||
is empty. When set to a non-empty URL template (supporting `{format}` and
|
||||
`{version}` placeholders) the reporter fires a single bounded POST after a
|
||||
successful apply, with a hard 5-second timeout and warning-level logging on
|
||||
failure. Failures never block the launch flow and are never retried.
|
||||
|
||||
The payload is intentionally small: `format`, `version`, `timestamp`. Nothing
|
||||
personally identifying.
|
||||
|
||||
## Backward compatibility
|
||||
|
||||
Existing installs pointing at a manifest without a `format` field continue to
|
||||
parse exactly as before: `Manifest.EffectiveFormat` defaults to
|
||||
`legacy-json-blob` and `LegacyJsonBlobFormat` is a near-verbatim lift of the
|
||||
old inline behaviour. The only visible difference is that the orchestrator's
|
||||
log line now reads `format=legacy-json-blob`.
|
||||
|
||||
Signed manifests that ship an unknown `format` value are refused outright —
|
||||
`ReleaseFormatFactory.Resolve` throws and the orchestrator reports
|
||||
`OfflineFallback`. This prevents a future attacker who compromises the signing
|
||||
key from downgrading clients via a fabricated `format: "no-verify"` field.
|
||||
|
||||
## File layout on disk
|
||||
|
||||
```
|
||||
client/
|
||||
├── Metin2.exe
|
||||
├── runtime-key.json ← written from the manifest, loaded by M2PackFormat
|
||||
├── pack/
|
||||
│ ├── item.m2p ← staged/placed by the launcher, never opened
|
||||
│ ├── mob.m2p
|
||||
│ └── ...
|
||||
└── .updates/
|
||||
├── launcher.log
|
||||
├── current-manifest.json
|
||||
└── staging/ ← per-file resume, same as legacy
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
Run `dotnet test -c Release` — the relevant suites are:
|
||||
|
||||
- `RuntimeKeyTests` — happy path, validation, load-from-disk
|
||||
- `EnvVarDeliveryTests` — scoped env mutation, no current-process leak
|
||||
- `LegacyJsonBlobFormatTests` / `M2PackFormatTests` / `ReleaseFormatFactoryTests`
|
||||
- `ClientAppliedReporterTests` — disabled-by-default, success, 500, timeout,
|
||||
connection refused
|
||||
- `UpdateOrchestratorFormatDispatchTests` — signature failure path, offline
|
||||
fallback when manifest fetch fails, result carries `Format` + `RuntimeKey`
|
||||
- Extended `GameProcessTests` asserting the runtime key gets forwarded through
|
||||
`BuildStartInfo` and does NOT leak into the launcher's own environment.
|
||||
|
||||
Total suite after this change is ~92 tests.
|
||||
Reference in New Issue
Block a user