Platformcom.zoa.modkit · v0.1.0

ZOA ModKit

Export a mod, sign it with RSA, and refuse to load one that fails your sandbox policy.

ModKit turns a folder of assets into a portable, hashed, optionally signed package, and gives the loading side the tools to decide whether it trusts what it just received. Four interfaces cover the lifecycle: export, import, sign and verify, and sandbox validation. Each has a shipped default implementation, and all four are swappable.

The design assumption is that the receiving end is the one that needs protecting. A manifest describes the mod, every asset carries a hash and a size, the signature is RSA over a deterministic payload, and a sandbox policy decides what content is even allowed to be present before anything is loaded.

Be clear about what the sandbox covers: it is content-based, not execution-based. It inspects declared asset types, relative paths and sizes against a policy. It does not run code in an isolated domain, and a policy that permits scripting is trusting the mod author.

Depends on (1)

Depended on by (0)

Nothing yet. This is a leaf.

How it works

Concepts

The manifest and the package

A ModManifest is the mod's identity: a ModId, display name, version, author, description, a game-version compatibility string, a dependency list, tags, and a created timestamp. ModId is a readonly struct with implicit conversions in both directions, so a string literal is a valid id.

Dependencies are ModDependency values pairing a ModId with a minimum version, which is enough for the loading side to order and gate installs without a resolver.

A ModExportPackage is what actually travels: the manifest, the asset entries, an optional signature, the total size, and the UTC export time. AddAsset appends an entry and keeps TotalSizeBytes correct, so the size is never separately maintained.

What an asset entry records

Each ModAssetEntry carries a relative path inside the package, an asset type derived from the file extension, a content hash, a size in bytes, and optionally the source path it was exported from.

The hash makes an import verifiable and gives the signature something to cover, since signing runs over a deterministic payload derived from the package rather than the raw bytes on disk. The sandbox validator inspects the relative path alongside the type.

Validation happens on both sides

ValidateForExport checks the manifest before you build a package: the id, display name, version and author must all be non-empty, each failure arriving as a coded issue such as MANIFEST_ID_EMPTY rather than an exception. Catching a missing author at export time is much cheaper than discovering it in someone else's install.

ValidateForImport checks the package on arrival: the manifest is present and has an id, the asset list is non-empty which is a warning rather than an error, the total size is not negative, and every asset has a path, a hash and a non-negative size. Codes such as ASSET_HASH_EMPTY and PACKAGE_NO_ASSETS make a failure diagnosable without reading the message.

A ModValidationResult accumulates ModValidationIssue values with a severity, a code, a message and the asset path they concern. IsValid reflects whether any error-severity issue is present, so a caller checks one flag and reads the issues only when it needs to explain.

Signing is RSA over a deterministic payload

GenerateKeyPair produces a public and private key pair as strings. Sign imports the private key, signs the package's deterministic payload with SHA256 and PKCS1 padding, and returns a ModSignature carrying the mod id, the base64 signature, the exported public key, the signing time, the algorithm string SHA256-RSA-PKCS1, and a validity flag.

Verify takes the package, the signature and the public key you expect. It first checks that the signature's embedded SignedByPublicKey matches the key you supplied, which is the step that stops a mod from vouching for itself with a key you have never seen, then verifies the signature against the recomputed payload.

The IsValid flag on ModSignature is a local convenience, set at signing time and carried inside the package. Real verification is a Verify call against a key you trust.

Sandbox policies are content policy

A SandboxPolicy is four booleans, a size cap and two namespace lists: AllowFileSystem, AllowNetworking, AllowReflection, AllowNativePlugins, MaxAssetSizeMb, AllowedNamespaces and BlockedNamespaces. Three factories cover the usual cases.

Strict denies everything, caps assets at 50MB, and blocks System.Reflection, System.Net, System.IO, UnityEngine.Networking and UnityEditor. Default denies the file system, reflection and native plugins but permits networking, caps at 200MB, and blocks System.Reflection, System.IO and UnityEditor. Permissive allows everything and caps at 500MB, which is the policy for content you authored yourself.

ValidateAsset applies the policy to one entry: size against the cap giving ASSET_TOO_LARGE, blocked namespaces matched against both the asset type and the relative path giving BLOCKED_NAMESPACE and BLOCKED_NAMESPACE_PATH, a whitelist check giving ASSET_NOT_IN_WHITELIST as a warning when AllowedNamespaces is non-empty and nothing matches, and a native plugin check giving NATIVE_PLUGIN_BLOCKED. ValidateMod runs the whole package.

Packages on disk

The editor adapter writes a package as a JSON document with the extension zoamod.json, alongside a payload directory holding the copied assets. CollectAssetFiles walks a source directory recursively, filters to exportable files, and returns them in a stable ordinal order so two exports of the same folder produce the same package.

ExportDirectoryToPackageFile is the one-call path from a folder to a package file. ExportToPackageFile takes an explicit asset list when you want to be selective, with an optional source root for computing relative paths and a flag controlling whether the payload is copied. LoadPackage reads one back, GetPayloadDirectory resolves where its assets live, and FormatPackageSummary produces the human-readable description the Workbench shows.

In the editor

Screens

Screenshot pending

/screenshots/modkit-export.png

The ModKit Export capability with a source folder chosen, the collected asset list showing paths, types and sizes, and the manifest fields filled in above a package summary.

Exporting a mod

Screenshot pending

/screenshots/modkit-sandbox.png

The Import capability after validating an untrusted package under the Strict policy: a list of issues with their codes (ASSET_TOO_LARGE, BLOCKED_NAMESPACE_PATH) and the affected asset paths, with the install action disabled.

Sandbox validation

Setup

Workflow

  1. 01

    Describe the mod

    Build a ModManifest with an id, display name, version and author, then add dependencies and tags. Run ValidateForExport before doing anything else; all four identity fields are required and each missing one comes back as a coded issue.

  2. 02

    Export

    From the ModKit page in the Workbench, or from code with ExportDirectoryToPackageFile, point at a source folder and a destination. The utility collects files recursively in a stable order, hashes each one, and writes a zoamod.json alongside a payload directory.

  3. 03

    Generate a key pair once

    GenerateKeyPair gives you a public and private key. Keep the private key out of the repository. The public key is what consumers pin, so publish it wherever they will look for it.

  4. 04

    Sign

    Sign the package with the private key and attach the resulting ModSignature. It records the mod id, the base64 signature, the public key, the signing time and the algorithm.

  5. 05

    Validate on the way in

    On the loading side, call ValidateForImport, then Verify against the public key you trust, then ValidateMod against your chosen SandboxPolicy. Do all three before Import, not after: importing first and validating second means the untrusted content is already on disk.

  6. 06

    Pick the right policy

    Strict for content from strangers, Default when the mod needs networking, Permissive only for content you authored. Adjust MaxAssetSizeMb and the namespace lists rather than reaching for Permissive when Strict rejects one thing.

Surface

Key types

IModExporter

interface

Builds a package from a manifest and a set of asset paths, and validates the manifest before you do.

  • ModExportPackage Export(ModManifest manifest, IEnumerable<string> assetPaths)
  • ModValidationResult ValidateForExport(ModManifest manifest)

IModImporter

interface

Installs a package to a target path, and validates it first. Validate before you import, not after.

  • ModImportResult Import(ModExportPackage package, string targetPath)
  • ModValidationResult ValidateForImport(ModExportPackage package)

IModSigningService

service

RSA key generation, signing over a deterministic package payload, and verification against a public key you supply.

  • ModSignature Sign(ModExportPackage package, string privateKey)
  • bool Verify(ModExportPackage package, ModSignature signature, string publicKey)
  • (string publicKey, string privateKey) GenerateKeyPair()

ISandboxValidator

interface

Applies a policy to a whole package or a single asset, and supplies the default policy for untrusted content.

  • ModValidationResult ValidateMod(ModExportPackage package, SandboxPolicy policy)
  • ModValidationResult ValidateAsset(ModAssetEntry asset, SandboxPolicy policy)
  • SandboxPolicy GetDefaultPolicy()

ModManifest

class

The mod's identity and compatibility metadata, including its dependency list and tags.

  • ModManifest(ModId id, string displayName, string version, string author)
  • ModId Id { get; set; }
  • string DisplayName, Version, Author, Description { get; set; }
  • string GameVersionCompatibility { get; set; }
  • List<ModDependency> Dependencies { get; set; }
  • List<string> Tags { get; set; }
  • DateTime CreatedUtc { get; set; }

ModId

struct

Readonly identifier with implicit conversions to and from string. Its default has a null Value, so check Id.Value rather than comparing the struct against null.

  • string Value { get; }

ModDependency

struct

A required mod and the minimum version that satisfies it.

  • ModId ModId { get; }
  • string MinVersion { get; }

ModExportPackage

class

What travels between machines: manifest, assets, optional signature, total size and export time. AddAsset keeps the total correct.

  • ModManifest Manifest { get; set; }
  • List<ModAssetEntry> Assets { get; set; }
  • ModSignature Signature { get; set; }
  • long TotalSizeBytes { get; set; }
  • DateTime ExportedAtUtc { get; set; }
  • void AddAsset(ModAssetEntry asset)

ModAssetEntry

class

One file in the package: its path inside the mod, its type, its content hash, its size, and where it came from.

  • ModAssetEntry(string relativePath, string assetType, string hash, long sizeBytes, string sourcePath = null)
  • string RelativePath { get; set; }
  • string AssetType { get; set; }
  • string Hash { get; set; }
  • long SizeBytes { get; set; }
  • string SourcePath { get; set; }

ModSignature

class

The signature over a package. SignedByPublicKey is the key Verify checks against the one you supply; IsValid is a local flag, not proof.

  • ModId ModId { get; set; }
  • string SignatureHash { get; set; }
  • string SignedByPublicKey { get; set; }
  • DateTime SignedAtUtc { get; set; }
  • string Algorithm { get; set; }
  • bool IsValid { get; set; }

SandboxPolicy

class

What a mod is allowed to contain. Three factories: Strict denies everything at 50MB, Default permits networking only at 200MB, Permissive allows everything at 500MB.

  • bool AllowFileSystem, AllowNetworking, AllowReflection, AllowNativePlugins { get; set; }
  • int MaxAssetSizeMb { get; set; }
  • List<string> AllowedNamespaces { get; set; }
  • List<string> BlockedNamespaces { get; set; }
  • static SandboxPolicy Strict()
  • static SandboxPolicy Default()
  • static SandboxPolicy Permissive()

ModValidationResult

class

Accumulated findings with an IsValid flag. AddIssue is how implementations report; codes make failures diagnosable.

  • bool IsValid { get; set; }
  • List<ModValidationIssue> Issues { get; set; }
  • void AddIssue(ValidationSeverity severity, string code, string message, string assetPath = null)

ModValidationIssue

class

One finding: severity, a stable code such as ASSET_TOO_LARGE, a message, and the asset it concerns.

  • ValidationSeverity Severity { get; set; }
  • string Code { get; set; }
  • string Message { get; set; }
  • string AssetPath { get; set; }

ModImportResult

class

Outcome of an install: whether it succeeded, where it landed, and what went wrong.

  • bool Success { get; set; }
  • string InstalledPath { get; set; }
  • List<ModValidationIssue> Issues { get; set; }

ModCompatibilityResult

class

Compatibility findings separate from validation findings, carrying ModCompatibilityIssue values with their own severity scale.

  • bool IsCompatible { get; set; }
  • List<ModCompatibilityIssue> Issues { get; set; }

ModExporter

class

The shipped exporter. Hashes each asset, derives its type from the file extension, and builds the package.

ModImporter

class

The shipped importer. Installs a validated package to a target path and reports where it landed.

ModSigningService

class

The shipped signing service. RSA with SHA256 and PKCS1 padding over a deterministic package payload; keys carry ZOA-RSA-PUBLIC and ZOA-RSA-PRIVATE prefixes.

SandboxValidator

class

The shipped validator. Checks size, blocked namespaces against both asset type and path, an optional whitelist, and native plugins.

ModKitWorkbenchPackageFileUtility

class

The editor adapter between the runtime services and files on disk. Writes and reads zoamod.json packages with a payload directory alongside.

  • const string PackageExtension = "zoamod.json"
  • static IReadOnlyList<string> CollectAssetFiles(string sourceDirectory)
  • static ModExportPackage ExportDirectoryToPackageFile(ModManifest manifest, string sourceDirectory, string packageFilePath, bool copyPayload = true)
  • static ModExportPackage ExportToPackageFile(ModManifest manifest, IEnumerable<string> assetPaths, string packageFilePath, string sourceRoot = null, bool copyPayload = true)
  • static void SavePackage(ModExportPackage package, string packageFilePath)
  • static ModExportPackage LoadPackage(string packageFilePath)
  • static string GetPayloadDirectory(string packageFilePath)
  • static string FormatPackageSummary(ModExportPackage package)

Usage

Examples

Export, validate and signcsharp
using System;
using ZOA.ModKit.Core;

var manifest = new ModManifest(
    id: "studio.nightfall.weapons",   // ModId converts implicitly from string
    displayName: "Nightfall Weapon Pack",
    version: "1.0.0",
    author: "Nightfall Studio")
{
    Description = "Twelve suppressed platforms and their attachments.",
    GameVersionCompatibility = ">=0.4.0",
};

manifest.Dependencies.Add(new ModDependency("zoa.core.ammo", "0.4.0"));
manifest.Tags.Add("weapons");

IModExporter exporter = new ModExporter();

// Catch a missing author here, not in someone else's install.
var manifestCheck = exporter.ValidateForExport(manifest);
if (!manifestCheck.IsValid)
{
    foreach (var issue in manifestCheck.Issues)
        Console.WriteLine($"[{issue.Severity}] {issue.Code}: {issue.Message}");
    return;
}

var package = exporter.Export(manifest, assetPaths);

IModSigningService signing = new ModSigningService();
var (publicKey, privateKey) = signing.GenerateKeyPair();
package.Signature = signing.Sign(package, privateKey);

Console.WriteLine($"{package} signed with {package.Signature.Algorithm}.");
Validate everything before importingcsharp
using ZOA.ModKit.Core;
using ZOA.ModKit.Unity.Editor.Workbench;

var package = ModKitWorkbenchPackageFileUtility.LoadPackage(packageFilePath);

IModImporter importer = new ModImporter();
IModSigningService signing = new ModSigningService();
ISandboxValidator sandbox = new SandboxValidator();

// 1. Structural validation.
var structure = importer.ValidateForImport(package);
if (!structure.IsValid)
    return Reject(structure);

// 2. Signature, against a key YOU pinned. Verify first checks that the
//    signature's embedded public key matches this one, so a mod cannot
//    vouch for itself with a key you have never seen.
if (package.Signature == null ||
    !signing.Verify(package, package.Signature, trustedPublicKey))
{
    return Reject("Signature missing or not signed by the trusted key.");
}

// 3. Content policy. Strict for anything from a stranger.
var policy = SandboxPolicy.Strict();
var sandboxResult = sandbox.ValidateMod(package, policy);
if (!sandboxResult.IsValid)
    return Reject(sandboxResult);

// Only now does untrusted content reach the disk.
var import = importer.Import(package, targetPath);
if (import.Success)
    LoadMod(import.InstalledPath);
The order matters: import first and validate second, and the content is on disk by the time you decide you did not want it.
Tuning a policy rather than loosening itcsharp
using ZOA.ModKit.Core;

// Start strict and widen deliberately, one decision at a time.
var policy = SandboxPolicy.Strict();

// This pack ships 4K textures; raise the cap rather than dropping to Permissive.
policy.MaxAssetSizeMb = 150;

// Whitelisting turns the check inside out: with a non-empty AllowedNamespaces,
// anything that matches neither the asset type nor the path prefix is flagged.
policy.AllowedNamespaces.Add("Weapons/");
policy.AllowedNamespaces.Add("Textures/");

ISandboxValidator sandbox = new SandboxValidator();

foreach (var asset in package.Assets)
{
    var result = sandbox.ValidateAsset(asset, policy);
    foreach (var issue in result.Issues)
    {
        // Codes are stable: ASSET_TOO_LARGE, BLOCKED_NAMESPACE,
        // BLOCKED_NAMESPACE_PATH, ASSET_NOT_IN_WHITELIST, NATIVE_PLUGIN_BLOCKED.
        System.Console.WriteLine($"{issue.Code} on {issue.AssetPath}: {issue.Message}");
    }
}

Tooling

Editor tools

ModKit (Workbench)

Workbench > System & Configuration > ModKit

Four capabilities: Export Mod packages a source folder into a zoamod.json bundle, Import Mod loads and validates a package before installing it, Signing Tools generates keys and signs and verifies packages, and Overview reports service status and package layout.

Read this

Notes and caveats

See also