From 43a2e9576f7d8a63e889ac6699d9b798febb8703 Mon Sep 17 00:00:00 2001 From: server Date: Tue, 14 Apr 2026 12:20:15 +0200 Subject: [PATCH] Add archive key id support --- README.md | 2 ++ docs/client-integration.md | 1 + docs/mcp.md | 2 ++ mcp_server.mjs | 10 ++++++++-- mcp_server.py | 6 ++++++ src/archive.cpp | 1 + src/archive.h | 4 +++- src/cli.cpp | 33 +++++++++++++++++++++++++++++---- 8 files changed, 52 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 307415a..5cd23e6 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,7 @@ Build an archive from a client asset directory: --output out/client.m2p \ --key keys/master.key \ --sign-secret-key keys/signing.key \ + --key-id 1 \ --json ``` @@ -147,6 +148,7 @@ Export a client config header for `m2dev-client-src/src/PackLib/M2PackKeys.h`: ./build/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 ``` diff --git a/docs/client-integration.md b/docs/client-integration.md index 07aa434..70acb65 100644 --- a/docs/client-integration.md +++ b/docs/client-integration.md @@ -40,6 +40,7 @@ Important: - the generated client header no longer embeds the real master key - `.m2p` loading now requires a runtime master key +- the runtime master key must match the archive `key_id` - if a `.m2p` file exists and fails validation or runtime key resolution, the client should not silently fall back to `.pck` ## Runtime validation diff --git a/docs/mcp.md b/docs/mcp.md index 8a01f17..6f0015c 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -43,6 +43,7 @@ Inputs: - `output_archive` - `key_file` - `signing_secret_key_file` +- `key_id` optional ### `pack_diff` @@ -80,6 +81,7 @@ Inputs: - `key_file` - `public_key_file` - `output_header` +- `key_id` optional ### `pack_binary_info` diff --git a/mcp_server.mjs b/mcp_server.mjs index f559e7d..475a8ef 100644 --- a/mcp_server.mjs +++ b/mcp_server.mjs @@ -79,8 +79,9 @@ server.tool( output_archive: z.string(), key_file: z.string(), signing_secret_key_file: z.string(), + key_id: z.number().int().positive().optional(), }, - async ({ input_dir, output_archive, key_file, signing_secret_key_file }) => + async ({ input_dir, output_archive, key_file, signing_secret_key_file, key_id }) => toolResult( runCli([ "build", @@ -92,6 +93,8 @@ server.tool( key_file, "--sign-secret-key", signing_secret_key_file, + "--key-id", + String(key_id ?? 1), ]), ), ); @@ -164,8 +167,9 @@ server.tool( key_file: z.string(), public_key_file: z.string(), output_header: z.string(), + key_id: z.number().int().positive().optional(), }, - async ({ key_file, public_key_file, output_header }) => + async ({ key_file, public_key_file, output_header, key_id }) => toolResult( runCli([ "export-client-config", @@ -173,6 +177,8 @@ server.tool( key_file, "--public-key", public_key_file, + "--key-id", + String(key_id ?? 1), "--output", output_header, ]), diff --git a/mcp_server.py b/mcp_server.py index dfbe863..eeb2de5 100644 --- a/mcp_server.py +++ b/mcp_server.py @@ -74,6 +74,7 @@ def pack_build( output_archive: str, key_file: str, signing_secret_key_file: str, + key_id: int = 1, ) -> dict[str, Any]: """Build an .m2p archive from a source directory.""" return _run_cli( @@ -86,6 +87,8 @@ def pack_build( key_file, "--sign-secret-key", signing_secret_key_file, + "--key-id", + str(key_id), ) @@ -139,6 +142,7 @@ def pack_export_client_config( key_file: str, public_key_file: str, output_header: str, + key_id: int = 1, ) -> dict[str, Any]: """Generate M2PackKeys.h for the Windows client tree.""" return _run_cli( @@ -147,6 +151,8 @@ def pack_export_client_config( key_file, "--public-key", public_key_file, + "--key-id", + str(key_id), "--output", output_header, ) diff --git a/src/archive.cpp b/src/archive.cpp index c0a113d..b76843f 100644 --- a/src/archive.cpp +++ b/src/archive.cpp @@ -168,6 +168,7 @@ BuildResult build_archive( std::memcpy(header.magic, kArchiveMagic, sizeof(kArchiveMagic)); header.version = kArchiveVersion; header.flags = 0; + header.key_id = keys.key_id; header.manifest_offset = sizeof(ArchiveHeader) + payload_bytes.size(); header.manifest_size = manifest_bytes.size(); std::memcpy(header.manifest_hash, manifest_hash.data(), manifest_hash.size()); diff --git a/src/archive.h b/src/archive.h index 680a907..e55354c 100644 --- a/src/archive.h +++ b/src/archive.h @@ -29,11 +29,12 @@ struct ArchiveHeader char magic[kMagicSize]; std::uint32_t version; std::uint32_t flags; + std::uint32_t key_id; std::uint64_t manifest_offset; std::uint64_t manifest_size; std::uint8_t manifest_hash[kHashSize]; std::uint8_t manifest_signature[kSignatureSize]; - std::uint8_t reserved[64]; + std::uint8_t reserved[60]; }; struct ManifestFixedHeader @@ -76,6 +77,7 @@ struct LoadedArchive struct KeyMaterial { + std::uint32_t key_id = 1; std::array master_key {}; std::optional> signing_secret_key; std::optional> signing_public_key; diff --git a/src/cli.cpp b/src/cli.cpp index 55faa4d..d6b6962 100644 --- a/src/cli.cpp +++ b/src/cli.cpp @@ -95,6 +95,23 @@ std::array load_master_key(const std::string& path) return key; } +std::uint32_t load_key_id(const ParsedArgs& args) +{ + const auto raw = optional_option(args, "key-id"); + if (!raw.has_value()) + { + return 1; + } + + const auto value = std::stoul(*raw); + if (value == 0) + { + fail("key-id must be greater than zero"); + } + + return static_cast(value); +} + struct SnapshotEntry { std::uint64_t size = 0; @@ -238,7 +255,7 @@ void print_usage() std::cout << "m2pack commands:\n" << " keygen --out-dir [--json]\n" - << " build --input --output --key --sign-secret-key [--json]\n" + << " build --input --output --key --sign-secret-key [--key-id ] [--json]\n" << " diff --left --right [--json]\n" << " list --archive [--json]\n" << " verify --archive [--public-key ] [--key ] [--json]\n" @@ -286,6 +303,7 @@ void command_keygen(const ParsedArgs& args) void command_build(const ParsedArgs& args) { KeyMaterial keys; + keys.key_id = load_key_id(args); keys.master_key = load_master_key(require_option(args, "key")); keys.signing_secret_key = load_hex_file(require_option(args, "sign-secret-key")); @@ -300,6 +318,7 @@ void command_build(const ParsedArgs& args) << "{" << "\"ok\":true," << "\"archive\":\"" << json_escape(result.archive_path.string()) << "\"," + << "\"key_id\":" << keys.key_id << "," << "\"file_count\":" << result.file_count << "," << "\"input_bytes\":" << result.total_input_bytes << "," << "\"stored_bytes\":" << result.total_stored_bytes @@ -318,7 +337,7 @@ void command_list(const ParsedArgs& args) if (args.json) { - std::cout << "{\"ok\":true,\"entries\":["; + std::cout << "{\"ok\":true,\"key_id\":" << archive.header.key_id << ",\"entries\":["; for (std::size_t i = 0; i < archive.entries.size(); ++i) { const auto& entry = archive.entries[i]; @@ -364,6 +383,7 @@ void command_verify(const ParsedArgs& args) std::cout << "{" << "\"ok\":" << (ok ? "true" : "false") << "," + << "\"key_id\":" << archive.header.key_id << "," << "\"entry_count\":" << archive.entries.size(); if (!ok) { @@ -447,12 +467,17 @@ void command_export_client_config(const ParsedArgs& args) << "// Do not edit manually.\n" << "// Runtime master key delivery is required for .m2p loading.\n\n" << "constexpr bool M2PACK_RUNTIME_MASTER_KEY_REQUIRED = true;\n\n" + << "constexpr uint32_t M2PACK_KEY_SLOT_COUNT = 1;\n" + << "constexpr std::array M2PACK_SIGN_KEY_IDS = { " + << load_key_id(args) + << " };\n\n" << "constexpr std::array M2PACK_MASTER_KEY = {" << render_array(zero_master) << "};\n\n" - << "constexpr std::array M2PACK_SIGN_PUBLIC_KEY = {" + << "constexpr std::array, M2PACK_KEY_SLOT_COUNT> M2PACK_SIGN_PUBLIC_KEYS = {{" + << "\n\t{" << render_array(public_key) - << "};\n"; + << "\t}\n}};\n"; const auto text = header.str(); write_file(output_path, std::vector(text.begin(), text.end()));