Add archive key id support

This commit is contained in:
server
2026-04-14 12:20:15 +02:00
parent aca8a675d2
commit 43a2e9576f
8 changed files with 52 additions and 7 deletions

View File

@@ -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
```

View File

@@ -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

View File

@@ -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`

View File

@@ -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,
]),

View File

@@ -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,
)

View File

@@ -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());

View File

@@ -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<std::uint8_t, kAeadKeySize> master_key {};
std::optional<std::vector<std::uint8_t>> signing_secret_key;
std::optional<std::vector<std::uint8_t>> signing_public_key;

View File

@@ -95,6 +95,23 @@ std::array<std::uint8_t, kAeadKeySize> 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<std::uint32_t>(value);
}
struct SnapshotEntry
{
std::uint64_t size = 0;
@@ -238,7 +255,7 @@ void print_usage()
std::cout
<< "m2pack commands:\n"
<< " keygen --out-dir <dir> [--json]\n"
<< " build --input <dir> --output <file> --key <hex-file> --sign-secret-key <hex-file> [--json]\n"
<< " build --input <dir> --output <file> --key <hex-file> --sign-secret-key <hex-file> [--key-id <n>] [--json]\n"
<< " diff --left <dir|archive.m2p> --right <dir|archive.m2p> [--json]\n"
<< " list --archive <file> [--json]\n"
<< " verify --archive <file> [--public-key <hex-file>] [--key <hex-file>] [--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<uint32_t, M2PACK_KEY_SLOT_COUNT> M2PACK_SIGN_KEY_IDS = { "
<< load_key_id(args)
<< " };\n\n"
<< "constexpr std::array<uint8_t, M2PACK_KEY_SIZE> M2PACK_MASTER_KEY = {"
<< render_array(zero_master)
<< "};\n\n"
<< "constexpr std::array<uint8_t, M2PACK_PUBLIC_KEY_SIZE> M2PACK_SIGN_PUBLIC_KEY = {"
<< "constexpr std::array<std::array<uint8_t, M2PACK_PUBLIC_KEY_SIZE>, 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<std::uint8_t>(text.begin(), text.end()));