Add runtime key export and launcher contract

This commit is contained in:
server
2026-04-14 12:26:22 +02:00
parent 43a2e9576f
commit cc1e4af651
7 changed files with 238 additions and 1 deletions

View File

@@ -25,6 +25,7 @@ with.
- `m2pack verify`
- `m2pack extract`
- `m2pack export-client-config`
- `m2pack export-runtime-key`
## MCP Server
@@ -63,6 +64,7 @@ Exposed tools:
- `pack_verify`
- `pack_extract`
- `pack_export_client_config`
- `pack_export_runtime_key`
- `pack_binary_info`
Smoke test:
@@ -152,6 +154,18 @@ Export a client config header for `m2dev-client-src/src/PackLib/M2PackKeys.h`:
--output /path/to/m2dev-client-src/src/PackLib/M2PackKeys.h
```
Export a runtime key payload for a launcher or CI handoff:
```bash
./build/m2pack export-runtime-key \
--key keys/master.key \
--public-key keys/signing.pub \
--key-id 1 \
--format json \
--output out/runtime-key.json \
--json
```
Diff a source tree against an archive:
```bash
@@ -172,3 +186,4 @@ Diff a source tree against an archive:
See [docs/format.md](docs/format.md) and
[docs/client-integration.md](docs/client-integration.md).
For Codex and Claude Code MCP setup, see [docs/agent-setup.md](docs/agent-setup.md).
For the runtime key payload contract, see [docs/launcher-contract.md](docs/launcher-contract.md).

View File

@@ -31,6 +31,7 @@ Generate it from the release key material with:
m2pack export-client-config \
--key keys/master.key \
--public-key keys/signing.pub \
--key-id 1 \
--output /path/to/m2dev-client-src/src/PackLib/M2PackKeys.h
```
@@ -71,6 +72,7 @@ Supported inputs, highest priority last:
--m2pack-key-hex <64-hex-master-key>
--m2pack-pubkey-hex <64-hex-public-key>
--m2pack-key-map <mapping-name>
--m2pack-key-id <integer>
```
### Environment
@@ -79,6 +81,7 @@ Supported inputs, highest priority last:
M2PACK_MASTER_KEY_HEX
M2PACK_SIGN_PUBKEY_HEX
M2PACK_KEY_MAP
M2PACK_KEY_ID
```
### Shared memory
@@ -96,6 +99,7 @@ struct M2PackSharedKeys {
char magic[8]; // "M2KEYS1\0"
uint32_t version; // 1
uint32_t flags; // reserved
uint32_t key_id; // runtime master key slot
uint8_t master_key[32];
uint8_t sign_public_key[32];
};

72
docs/launcher-contract.md Normal file
View File

@@ -0,0 +1,72 @@
# Launcher contract
`m2pack` can export a runtime key payload for the Windows client loader.
That payload is meant for a launcher, bootstrapper, or CI handoff step that
delivers the active release key material at runtime.
## Command
```bash
./build/m2pack export-runtime-key \
--key keys/master.key \
--public-key keys/signing.pub \
--key-id 1 \
--format json \
--output out/runtime-key.json \
--json
```
Options:
- `--key`
- `--public-key`
- `--key-id` optional, defaults to `1`
- `--format json|blob` optional, defaults to `json`
- `--output`
## JSON format
Use this for CI, scripts, and launcher preprocessing:
```json
{
"version": 1,
"mapping_name": "Local\\M2PackSharedKeys",
"key_id": 1,
"master_key_hex": "<64 hex chars>",
"sign_public_key_hex": "<64 hex chars>"
}
```
## Binary format
Use this when a launcher wants to write the exact shared-memory payload expected
by the client:
```c
struct M2PackSharedKeys {
char magic[8]; // "M2KEYS1\0"
uint32_t version; // 1
uint32_t flags; // reserved
uint32_t key_id; // runtime master key slot
uint8_t master_key[32];
uint8_t sign_public_key[32];
};
```
The client currently expects:
- `magic = "M2KEYS1\0"`
- `version = 1`
- `flags = 0`
- `key_id` matching the archive header `key_id`
## Recommended flow
1. Linux CI builds `.m2p` with `m2pack build --key-id <n>`.
2. Linux CI exports `M2PackKeys.h` with `m2pack export-client-config`.
3. Linux CI exports a runtime key payload with `m2pack export-runtime-key`.
4. The Windows launcher creates `Local\\M2PackSharedKeys`.
5. The launcher writes the blob and starts the client with `--m2pack-key-map`.
6. The client rejects `.m2p` loading if the runtime key is missing or the `key_id` does not match.

View File

@@ -83,6 +83,16 @@ Inputs:
- `output_header`
- `key_id` optional
### `pack_export_runtime_key`
Inputs:
- `key_file`
- `public_key_file`
- `output_file`
- `key_id` optional
- `format` optional, `json` or `blob`
### `pack_binary_info`
No input. Returns the active `m2pack` binary path.

View File

@@ -185,6 +185,34 @@ server.tool(
),
);
server.tool(
"pack_export_runtime_key",
"Generate a launcher runtime key payload in json or blob form.",
{
key_file: z.string(),
public_key_file: z.string(),
output_file: z.string(),
key_id: z.number().int().positive().optional(),
format: z.enum(["json", "blob"]).optional(),
},
async ({ key_file, public_key_file, output_file, key_id, format }) =>
toolResult(
runCli([
"export-runtime-key",
"--key",
key_file,
"--public-key",
public_key_file,
"--key-id",
String(key_id ?? 1),
"--format",
format ?? "json",
"--output",
output_file,
]),
),
);
server.tool(
"pack_binary_info",
"Report which m2pack binary the server will execute.",

View File

@@ -158,6 +158,30 @@ def pack_export_client_config(
)
@mcp.tool()
def pack_export_runtime_key(
key_file: str,
public_key_file: str,
output_file: str,
key_id: int = 1,
format: str = "json",
) -> dict[str, Any]:
"""Generate a launcher runtime key payload in json or blob form."""
return _run_cli(
"export-runtime-key",
"--key",
key_file,
"--public-key",
public_key_file,
"--key-id",
str(key_id),
"--format",
format,
"--output",
output_file,
)
@mcp.tool()
def pack_binary_info() -> dict[str, Any]:
"""Report which m2pack binary the server will execute."""

View File

@@ -120,6 +120,18 @@ struct SnapshotEntry
using SnapshotMap = std::map<std::string, SnapshotEntry>;
#pragma pack(push, 1)
struct LauncherSharedKeysBlob
{
char magic[8];
std::uint32_t version;
std::uint32_t flags;
std::uint32_t key_id;
std::uint8_t master_key[kAeadKeySize];
std::uint8_t sign_public_key[crypto_sign_PUBLICKEYBYTES];
};
#pragma pack(pop)
SnapshotMap snapshot_from_directory(const std::filesystem::path& root)
{
SnapshotMap snapshot;
@@ -260,7 +272,8 @@ void print_usage()
<< " list --archive <file> [--json]\n"
<< " verify --archive <file> [--public-key <hex-file>] [--key <hex-file>] [--json]\n"
<< " extract --archive <file> --output <dir> --key <hex-file> [--json]\n"
<< " export-client-config --key <hex-file> --public-key <hex-file> --output <file> [--json]\n";
<< " export-runtime-key --key <hex-file> --public-key <hex-file> --output <file> [--key-id <n>] [--format json|blob] [--json]\n"
<< " export-client-config --key <hex-file> --public-key <hex-file> --output <file> [--key-id <n>] [--json]\n";
}
void command_keygen(const ParsedArgs& args)
@@ -495,6 +508,72 @@ void command_export_client_config(const ParsedArgs& args)
std::cout << "Wrote client config to " << output_path << "\n";
}
void command_export_runtime_key(const ParsedArgs& args)
{
const auto master = load_hex_file(require_option(args, "key"));
const auto public_key = load_hex_file(require_option(args, "public-key"));
const auto output_path = require_option(args, "output");
const auto format = optional_option(args, "format").value_or("json");
const auto key_id = load_key_id(args);
if (master.size() != kAeadKeySize)
{
fail("Master key must be 32 bytes");
}
if (public_key.size() != crypto_sign_PUBLICKEYBYTES)
{
fail("Signing public key has invalid size");
}
if (format == "json")
{
std::ostringstream out;
out
<< "{\n"
<< " \"version\": 1,\n"
<< " \"mapping_name\": \"Local\\\\M2PackSharedKeys\",\n"
<< " \"key_id\": " << key_id << ",\n"
<< " \"master_key_hex\": \"" << to_hex(master) << "\",\n"
<< " \"sign_public_key_hex\": \"" << to_hex(public_key) << "\"\n"
<< "}\n";
const auto text = out.str();
write_file(output_path, std::vector<std::uint8_t>(text.begin(), text.end()));
}
else if (format == "blob")
{
LauncherSharedKeysBlob blob {};
std::memcpy(blob.magic, "M2KEYS1", 8);
blob.version = 1;
blob.flags = 0;
blob.key_id = key_id;
std::memcpy(blob.master_key, master.data(), master.size());
std::memcpy(blob.sign_public_key, public_key.data(), public_key.size());
std::vector<std::uint8_t> bytes(sizeof(blob));
std::memcpy(bytes.data(), &blob, sizeof(blob));
write_file(output_path, bytes);
}
else
{
fail("Unsupported format for export-runtime-key: " + format);
}
if (args.json)
{
std::cout
<< "{"
<< "\"ok\":true,"
<< "\"output\":\"" << json_escape(output_path) << "\","
<< "\"format\":\"" << json_escape(format) << "\","
<< "\"key_id\":" << key_id
<< "}\n";
return;
}
std::cout << "Wrote runtime key payload to " << output_path << "\n";
}
} // namespace
int run_cli(int argc, char** argv)
@@ -544,6 +623,11 @@ int run_cli(int argc, char** argv)
command_export_client_config(args);
return 0;
}
if (args.command == "export-runtime-key")
{
command_export_runtime_key(args);
return 0;
}
if (args.command == "help" || args.command == "--help" || args.command == "-h")
{
print_usage();