Back to Blog

Inside the Gatekeeper: How MPP Verifies Every Tool Before Execution

MPP Protocol·February 27, 2026·11 min read
MPPSecurityGatekeeperEd25519WASMTechnical

Every security model has a single point where the decision is made: does this code run, or doesn't it? In MPP, that decision point is the Gatekeeper — a multi-step verification pipeline that every package must pass before the WASM sandbox is instantiated.

The Gatekeeper is not a single check. It is seven sequential checks, each targeting a different class of attack. If any step fails, execution is refused. There is no override, no "run anyway" flag, no way for a tool to skip the line.

This post walks through each step, explains the threat it mitigates, and shows what happens at the code level.


Why Sequential Checks Matter

A common pattern in security engineering is to combine multiple checks into a single pass: validate the input, check the signature, parse the manifest, all in one function. This is efficient but brittle. If the checks are interdependent and one produces an incorrect result, the cascade can skip downstream protections.

The Gatekeeper takes a different approach. Each step is independent. Each step has a clear input, a clear output, and a clear failure mode. The output of one step feeds the next, but a failure at any point terminates the pipeline. This means:

  • A malformed archive never reaches the signature check.
  • A tampered package never reaches manifest validation.
  • An unsigned package is flagged before capabilities are evaluated.

Defence in depth, applied to the verification pipeline itself.


Step 1: Archive Safety

Threat mitigated: Zip bombs, resource exhaustion, filesystem escape.

The .mpp package format is a standard ZIP archive. ZIP is well-understood and broadly supported, but it also has a long history of being weaponised. Zip bombs — archives that decompress to thousands of times their compressed size — can crash or freeze any system that naively extracts them. Path traversal attacks embed filenames like ../../etc/cron.d/backdoor to write files outside the intended directory.

The Gatekeeper's first step validates the archive against six constraints before decompressing anything:

| Check | Threshold | Attack Prevented | |-------|-----------|-----------------| | Compressed file size | ≤ 256 MB | Resource exhaustion | | Entry count | ≤ 1,000 files | Filesystem abuse | | Compression ratio | ≤ 100:1 per entry | Zip bomb | | Path characters | UTF-8, forward slashes only | Encoding attacks | | Path traversal | No .., no absolute paths, no \ | Directory escape | | Total uncompressed size | ≤ 256 MB | Memory exhaustion |

These checks run against the archive's central directory — the metadata that describes the archive's contents — without extracting files to disk. If the archive advertises an entry with a 25 GB uncompressed size, the Gatekeeper rejects it before reading a single byte of compressed data.

archive.mpp
  ├── manifest.json         ✓ Valid UTF-8 path
  ├── bin/tool.wasm         ✓ No traversal
  ├── resources/config.json ✓ Under entry limit
  └── certs/signature.sig   ✓ Ratio check passed

A file named ../../../etc/passwd? Rejected. An archive with 100,000 entries? Rejected. A 1 KB file that decompresses to 100 GB? Rejected.

This step exists because every subsequent step requires reading archive contents. If the archive itself is malicious, nothing downstream matters.


Step 2: Manifest Parsing

Threat mitigated: Malformed input, missing required fields, invalid runtime targets.

Once the archive passes safety checks, the Gatekeeper extracts and parses manifest.json. This is a strict parse — not a best-effort extraction of whatever fields happen to be present, but a validation against a defined schema.

Required fields:

  • mpp_version — protocol version (currently "0.1.0")
  • package_id — unique reverse-DNS identifier (e.g., org.example.my-tool)
  • version — SemVer version string
  • name — human-readable tool name
  • publisher — publisher identity including name and public_key
  • runtime — must specify wasm32-wasi with a valid entrypoint matching bin/*.wasm
  • capabilities — resource declarations (network, filesystem, env vars)
  • tool_definitions — at least one tool with name, description, and parameters schema

If any required field is missing, if the version is not valid SemVer, or if the runtime type is not wasm32-wasi, the pipeline terminates here.

This step exists because every downstream component — the signature verifier, the permission engine, the sandbox — depends on a well-formed manifest. Catching structural problems early prevents confusing failures later.


Step 3: Content Hash Computation

Threat mitigated: Integrity violations, partial tampering.

The Gatekeeper computes a SHA-256 hash over the entire package contents to create a deterministic fingerprint. The algorithm:

  1. List all entries in the archive excluding the certs/ directory (which contains the signature itself — you can't include the signature in the data being signed).
  2. Sort entries alphabetically by path.
  3. For each entry, hash the path (UTF-8 bytes) followed by the entry content.
  4. Produce a final 32-byte SHA-256 digest.
content_hash = SHA-256(
  sorted([
    path_bytes("bin/tool.wasm") + file_bytes("bin/tool.wasm"),
    path_bytes("manifest.json") + file_bytes("manifest.json"),
    path_bytes("resources/config.json") + file_bytes("resources/config.json"),
  ])
)

The sorting is critical. ZIP archives do not guarantee entry order — the same set of files can be archived in different orders. By sorting before hashing, the content hash is deterministic: the same contents always produce the same hash, regardless of how the archive was constructed.

This hash is what the publisher signed, and what the Gatekeeper will verify in the next step. If even a single byte of any file in the package has been changed since signing — a modified binary, an altered manifest, an inserted resource — the hash will not match.


Step 4: Signature Verification

Threat mitigated: Supply-chain tampering, man-in-the-middle modification, package substitution.

This is the core trust check. The Gatekeeper reads two files from the certs/ directory:

  • certs/publisher.pub — the publisher's Ed25519 public key (32 bytes, base64-encoded)
  • certs/signature.sig — the Ed25519 signature over the content hash (64 bytes, base64-encoded)

It then performs the verification:

Ed25519.verify(publisher.pub, content_hash, signature.sig)

If the signature is valid, it proves two things simultaneously:

  1. Integrity. The package contents have not been modified since the publisher signed them. The content hash matches what was signed.
  2. Authorship. The package was signed by the holder of the private key corresponding to publisher.pub. No one else could have produced this signature.

Ed25519 was chosen for specific engineering reasons. It is fast (~60μs per verification), produces small signatures (64 bytes), requires no configuration parameters (no choice of curve, hash, or padding to get wrong), is deterministic (same input always produces the same signature), and has a battle-tested Rust implementation via the ring library with no OpenSSL dependency.

What happens if a package is unsigned? The Gatekeeper does not reject unsigned packages outright — it marks them as unverified. The host runtime can then enforce its own policy: some deployments may reject unverified packages entirely, others may allow them with reduced trust or additional HITL confirmation. The Gatekeeper provides the signal; the host decides the policy.


Step 5: Trust Check

Threat mitigated: Unknown publishers, untrusted sources.

A valid signature proves that the package was signed by a specific key. But a valid signature from an unknown key is not, by itself, a reason to trust the package. Step 5 checks whether the publisher's public key is trusted by the host.

Trust can be established through several mechanisms:

  • Manual configuration. An administrator adds the publisher's public key to the host's trusted keystore. This is the most direct model — the organisation explicitly decides which publishers it trusts.
  • Registry verification. The publisher registered their key with a trusted MPP registry, which verified their identity before accepting the key. The host trusts the registry, and transitively trusts the publisher.
  • Auto-trust policy. For low-security tools with no capability declarations, the host may be configured to auto-trust any properly signed package. This is a convenience for evaluation and development environments.

The Gatekeeper exposes a simple API for trust management:

let mut gatekeeper = Gatekeeper::new();

// Manually trust a publisher
gatekeeper.trust_key(publisher_public_key_bytes);

// Check trust status
assert!(gatekeeper.is_key_trusted(&publisher_public_key_bytes));

This step is the bridge between cryptographic verification and organisational policy. The signature says "this package is intact and came from key X." The trust check says "we have decided to trust key X."


Step 6: Manifest Validation

Threat mitigated: Schema violations, malformed capability declarations, invalid tool definitions.

Step 2 parsed the manifest for basic structural correctness. Step 6 performs a full schema validation — a deeper check that ensures every field not only exists but conforms to the expected format and constraints.

This includes:

  • Capability declarations are well-formed. Network domains are valid FQDNs. Filesystem paths are absolute and do not contain traversal sequences. Environment variable names are non-empty strings.
  • Tool definitions have valid security levels. Each tool declares a security level — low, medium, high, or critical — that feeds into the HITL sensitivity scoring system. An invalid security level is rejected.
  • Custom privacy patterns are within length limits. To prevent ReDoS (Regular Expression Denial of Service) attacks, custom regex patterns are limited to 512 characters and are validated against a 100ms execution timeout.
  • The entrypoint WASM binary exists. The path declared in runtime.entrypoint must match an actual entry in the archive's bin/ directory.

Why is this separate from Step 2? Because Step 2 catches malformed JSON and missing required fields — problems that prevent parsing. Step 6 catches semantically invalid manifests — manifests that parse correctly but declare things that don't make sense or violate safety constraints. Running full schema validation after signature verification ensures that the manifest being validated is the manifest the publisher signed.


Step 7: Return Result

Output: The verified package, ready for execution.

If all six preceding steps pass, the Gatekeeper produces an AttestationResult:

pub struct AttestationResult {
    pub manifest: Manifest,           // Validated manifest
    pub wasm_binary: Vec<u8>,         // WASM binary bytes
    pub content_hash: [u8; 32],       // SHA-256 content hash
    pub signature_valid: bool,        // Was the signature valid?
    pub resources: Vec<(String, Vec<u8>)>,  // Additional resource files
}

This structure gives the host runtime everything it needs to proceed:

  • The manifest drives the permission engine (what capabilities does this tool need?) and the sandbox configuration (what resources should be mounted?).
  • The WASM binary is loaded into the Wasmtime engine.
  • The content hash is logged for audit purposes.
  • The signature_valid flag lets the host enforce policy on unsigned packages.
  • The resources are any additional files bundled in the package that the tool may need at runtime.

The AttestationResult is the handoff point between verification and execution. Everything upstream is about answering the question "should this tool run?" Everything downstream is about running it safely.


The Complete Pipeline in Context

The Gatekeeper is the first stage of a four-stage execution pipeline:

┌──────────────┐     ┌──────────────┐     ┌──────────────┐     ┌──────────────┐
│  GATEKEEPER  │ ──→ │  PERMISSION  │ ──→ │   SANDBOX    │ ──→ │   PRIVACY    │
│  (Verify)    │     │  (Approve)   │     │  (Execute)   │     │  (Filter)    │
└──────────────┘     └──────────────┘     └──────────────┘     └──────────────┘

After the Gatekeeper, the permission engine evaluates the tool's declared capabilities against host policy and optionally triggers human-in-the-loop confirmation. Then the WASM sandbox executes the tool with only the approved capabilities. Finally, the privacy filter redacts sensitive data from the response before it reaches the agent.

Each stage is independent. Each stage can reject the invocation. Each stage adds a layer of protection. But it all starts with the Gatekeeper — because none of those downstream protections matter if the package itself has been tampered with.


Performance

A common concern with multi-step verification is latency. Seven sequential checks sound expensive.

In practice, the Gatekeeper completes in low single-digit milliseconds for typical packages. Archive safety checks run against the ZIP central directory without extracting files. SHA-256 hashing and Ed25519 verification are both sub-millisecond operations. Manifest parsing and schema validation operate on small JSON documents.

For frequently-invoked tools, hosts can cache the AttestationResult and skip re-verification entirely — valid because the content hash is deterministic and the signature covers immutable content. A cached verification is a HashMap lookup: effectively free.

The Gatekeeper adds milliseconds to the first invocation and zero to subsequent invocations. The security guarantees it provides are worth orders of magnitude more.


For a complete reference of the Gatekeeper API, see the Runtime API Reference. To understand the security pipeline that follows the Gatekeeper, read Zero Trust for AI Agents.