Add runtime key export and launcher contract
This commit is contained in:
15
README.md
15
README.md
@@ -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).
|
||||
|
||||
@@ -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
72
docs/launcher-contract.md
Normal 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.
|
||||
10
docs/mcp.md
10
docs/mcp.md
@@ -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.
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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."""
|
||||
|
||||
86
src/cli.cpp
86
src/cli.cpp
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user