Formal JSON schema for the release manifest, with canonical ordering rules so signatures stay stable. Includes a small synthetic example under docs/examples/.
125 lines
5.4 KiB
Markdown
125 lines
5.4 KiB
Markdown
# Update manifest — format specification
|
|
|
|
The update manifest is a JSON document describing a single release of the Metin2 client. It lives at `https://updates.jakubkadlec.dev/manifest.json` alongside its Ed25519 signature at `manifest.json.sig`.
|
|
|
|
See [update-manager.md](./update-manager.md) for the overall architecture this fits into.
|
|
|
|
## Top-level schema
|
|
|
|
```json
|
|
{
|
|
"version": "2026.04.14-1",
|
|
"created_at": "2026-04-14T12:00:00Z",
|
|
"previous": "2026.04.13-3",
|
|
"launcher": {
|
|
"path": "Metin2Launcher.exe",
|
|
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
"size": 15728640
|
|
},
|
|
"files": [
|
|
{
|
|
"path": "Metin2.exe",
|
|
"sha256": "a1b2c3...",
|
|
"size": 27982848,
|
|
"platform": "windows",
|
|
"required": true
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
### Required top-level fields
|
|
|
|
| Field | Type | Description |
|
|
|---|---|---|
|
|
| `version` | string | Release version. Format: `YYYY.MM.DD-N` where `N` is the 1-indexed daily counter. Sortable, human-readable, allows multiple releases per day. |
|
|
| `created_at` | string | ISO 8601 timestamp in UTC with `Z` suffix. When the release was produced. |
|
|
| `launcher` | object | The launcher binary. See below. Special because of self-update handling. |
|
|
| `files` | array | The non-launcher files in the release. May be empty (launcher-only update). |
|
|
|
|
### Optional top-level fields
|
|
|
|
| Field | Type | Description |
|
|
|---|---|---|
|
|
| `previous` | string | `version` of the manifest this release replaces. Omit for the first release ever. Used for changelog display and future delta-patch strategies. |
|
|
| `notes` | string | Free-form release notes (Markdown). Displayed by the launcher in the changelog panel. |
|
|
| `min_launcher_version` | string | Refuse to apply this manifest if launcher's own version is older than this. Used when a manifest change requires a newer launcher. |
|
|
|
|
## File entry schema
|
|
|
|
```json
|
|
{
|
|
"path": "pack/item.pck",
|
|
"sha256": "def456abc123...",
|
|
"size": 128000000,
|
|
"platform": "all",
|
|
"required": true
|
|
}
|
|
```
|
|
|
|
### Required file fields
|
|
|
|
| Field | Type | Description |
|
|
|---|---|---|
|
|
| `path` | string | Path relative to the client root, using forward slashes. No `..` segments. |
|
|
| `sha256` | string | Lowercase hex sha256 of the file contents. |
|
|
| `size` | integer | File size in bytes. Used for the progress bar and to detect truncated downloads before hashing. |
|
|
|
|
### Optional file fields
|
|
|
|
| Field | Type | Default | Description |
|
|
|---|---|---|---|
|
|
| `platform` | string | `"all"` | One of `"windows"`, `"linux"`, `"all"`. Launcher filters by its own platform. |
|
|
| `required` | boolean | `true` | If `false`, a failed download for this file does not block the game launch. |
|
|
| `executable` | boolean | `false` | On Unix-like systems, set the executable bit after applying. Ignored on Windows. |
|
|
|
|
## Launcher entry
|
|
|
|
The `launcher` top-level object has the same fields as a file entry, but is called out separately because the launcher is a privileged file:
|
|
|
|
- The launcher replaces itself via **rename-before-replace**, not normal atomic move.
|
|
- The launcher is **always required**; if it fails to update, the launcher does not launch the game, to avoid a broken loop where the player is running a buggy launcher that can't fix itself.
|
|
- The launcher is **never** listed in the `files` array.
|
|
|
|
## Signing
|
|
|
|
`manifest.json.sig` is the raw Ed25519 signature over the literal bytes of `manifest.json`, in detached form. The public key is compiled into the launcher binary. Signing and verification use the standard Ed25519 algorithm (RFC 8032), no prehashing.
|
|
|
|
Example verification in Python:
|
|
|
|
```python
|
|
import json
|
|
from nacl.signing import VerifyKey
|
|
|
|
with open("manifest.json", "rb") as f:
|
|
manifest_bytes = f.read()
|
|
with open("manifest.json.sig", "rb") as f:
|
|
sig = f.read()
|
|
|
|
VerifyKey(bytes.fromhex(PUBLIC_KEY_HEX)).verify(manifest_bytes, sig)
|
|
```
|
|
|
|
In C# with `System.Security.Cryptography` (.NET 8+) or BouncyCastle.
|
|
|
|
## Canonical JSON
|
|
|
|
To keep signatures stable across trivial reformatting:
|
|
|
|
- Top-level keys appear in the order `version, created_at, previous, notes, min_launcher_version, launcher, files`.
|
|
- Within the `files` array, entries are **sorted by `path`** lexicographically.
|
|
- Within a file object, keys appear in the order `path, sha256, size, platform, required, executable`.
|
|
- JSON is pretty-printed with **2-space indentation**, **LF line endings**, final newline.
|
|
- Strings use the shortest valid JSON escapes (no `\u00XX` for printable ASCII).
|
|
|
|
`scripts/make-manifest.py` produces output in exactly this form. Do not re-serialize a manifest with a different JSON library before signing; the bytes must match.
|
|
|
|
## Versioning rules
|
|
|
|
- `version` strings are compared **as date + counter** (not as semver), via `(date, counter)` tuples.
|
|
- A launcher always replaces its own installed version with the one from the latest manifest, regardless of whether the manifest's `version` is newer than the launcher's own version. There is no "downgrade protection" for the launcher itself because the server is the source of truth.
|
|
- For the **client files** (not launcher), the launcher refuses to apply a manifest whose `version` is older than the locally-recorded `current-manifest.json` version. This prevents rollback attacks where a compromised CDN replays an old manifest to force players back onto an outdated client that had a known vulnerability.
|
|
|
|
## Example
|
|
|
|
See [examples/manifest-example.json](./examples/manifest-example.json) for a real manifest produced by `scripts/make-manifest.py` over the current dev client.
|