AID-H-028.006: Client Distribution, Plugin & Update Integrity Verification

Version 1.20261015 · Actionable sub-technique

Verify the integrity and authenticity of the MCP client binary, its plugins or extensions, and all updates before installation or execution. This sub-technique prevents supply chain attacks against the client distribution channel, including tampered binaries, malicious plugins masquerading as legitimate extensions, and compromised update infrastructure. Verification uses code signing, artifact verification, provenance or attestation where available, and update-channel integrity controls such as TUF-backed metadata and rollback protection.

Pillars: infra

Lifecycle: building, operation

Scope boundary

Owns authenticity, integrity, provenance, update-channel security, and rollback protection for the MCP client binary, its plugins or extensions, and updates before installation or execution.

AID-H-003.001: AID-H-028.006 specializes artifact verification for the MCP client distribution and update path; AID-H-003.001 makes the broader admission decision for exact third-party components consumed by build or deployment. A valid client signature is an input to supply-chain admission, not a general component approval.

AID-H-023: AID-H-028.006 verifies the distributed client, plugin, or update artifact; AID-H-023 hardens the publisher identity and release workflow that produced a package. Trusted publishing and consumer-side artifact verification are distinct links in the chain.

AID-H-003.010: AID-H-028.006 verifies authenticity, integrity, provenance, and rollback protection before an MCP client, plugin, or update executes; AID-H-003.010 decides whether and when deployed AI software requires vulnerability remediation and verifies fleet completion. An authentic update can remain vulnerable or undeployed, while remediation policy never permits an unverified update.

Implementation guidance

AID-H-028.006-G001

Verify signatures or trusted identities for client binaries and plugins before installation or first execution.
Concept:

Require a signature or trusted publisher identity for every distributable artifact. Do not install or load unsigned artifacts unless there is a tightly controlled break-glass exception.

Example cosign verification for a downloaded artifact
cosign verify-blob \
  --bundle client.sigstore.json \
  --certificate-identity build-bot@example.com \
  --certificate-oidc-issuer https://github.com/login/oauth \
  client-binary.zip

Operational notes: On macOS and Windows, also verify platform-native signatures such as notarization or Authenticode during install. Store trusted publisher identities in a reviewed allowlist.

Production implementation

At installer and execution admission, hash the exact bytes, verify the policy-approved signature, publisher identity and provenance, reject tag-only or unsigned artifacts and bind the admitted digest to the installed path.

Independent verification

A separately credentialed verifier independently reads back or replays downloaded bytes, signature/provenance and installed-file digest with tampered, unsigned, wrong-publisher, stale-signature and post-install mutation fixtures.

AID-H-028.006-G002

Protect the update channel with signed metadata, rollback protection, and pinned update sources.
Concept:

The client should not trust an update just because it came from a URL. Use signed update metadata and reject version rollback or source drift.

Example checks your updater should enforce
{
  "trusted_update_roots": ["https://updates.example.com/tuf/"],
  "minimum_allowed_version": "3.4.0",
  "require_signed_targets": true,
  "reject_rollback": true
}

Operational notes: TUF is a strong fit for update metadata because it separates roles, supports key rotation, and helps detect rollback. Even if you use another updater, keep the same properties: signed metadata, monotonic version checks, pinned source, and staged rollout.

Production implementation

At update resolution, verify threshold-signed metadata, root/timestamp/snapshot/targets versions, target digest and length, approved source and rollback floor; install atomically and refuse stale, frozen, mixed-generation or unlisted targets.

Independent verification

A separately credentialed verifier independently reads back or replays metadata chain, target bytes, rollback counter and installed version with expired, freeze, rollback, mix-and-match, mirror and interrupted-install fixtures.

AID-H-028.006-G003

Continuously inventory installed client plugins and compare them against an approved allowlist with version and publisher constraints.
Concept:

Validate the plugin population the client loader will actually use, not a plugin-authored inventory file. The host-specific adapter must take a complete pre-activation snapshot, copy each discovered package into a protected immutable staging area, and keep plugin execution disabled until this gate returns PASS. A signature-verified allowlist binds the loader key, publisher, exact version, and SHA-256 of every approved package; the digest is the primary identity. If policy permits version ranges, resolve the range to approved exact versions and digests in the policy service before this validator runs.

Reusable pre-load validator
# File: client_plugin_policy/admission.py
from __future__ import annotations

import hashlib
import os
import stat
from dataclasses import dataclass
from pathlib import Path
from typing import Protocol, Sequence


@dataclass(frozen=True)
class PluginRule:
    loader_key: str
    publisher: str
    version: str
    sha256: str
    required: bool = False


@dataclass(frozen=True)
class PluginPolicy:
    revision: str
    trusted_snapshot_roots: Sequence[str]
    rules: Sequence[PluginRule]


@dataclass(frozen=True)
class PluginCandidate:
    loader_key: str
    publisher: str
    version: str
    snapshot_path: str


@dataclass(frozen=True)
class PluginSnapshot:
    snapshot_id: str
    phase: str
    population_complete: bool
    activation_disabled: bool
    loader_population: Sequence[str]
    candidates: Sequence[PluginCandidate]


class VerifiedPolicyProvider(Protocol):
    def load_verified(self) -> PluginPolicy:
        """Verify policy signature, schema, validity window, and revision."""
        raise RuntimeError("protocol-only method")


class HostPluginInventory(Protocol):
    def snapshot_before_activation(self) -> PluginSnapshot:
        """Enumerate the loader population and snapshot exact package bytes."""
        raise RuntimeError("protocol-only method")

    def keep_disabled_and_readback(self, loader_keys: Sequence[str]) -> str:
        """Disable rejected plugins and return the effective loader-state receipt."""
        raise RuntimeError("protocol-only method")


class EvidenceSink(Protocol):
    def write_and_readback(self, report: dict) -> str:
        raise RuntimeError("protocol-only method")


def _under_trusted_root(path: Path, roots: Sequence[str]) -> bool:
    resolved = path.resolve(strict=True)
    for raw_root in roots:
        root = Path(raw_root).resolve(strict=True)
        try:
            resolved.relative_to(root)
            return True
        except ValueError:
            pass
    return False


def _hash_regular_snapshot(path_text: str, roots: Sequence[str]) -> str:
    path = Path(path_text)
    if not _under_trusted_root(path, roots):
        raise RuntimeError("plugin_snapshot_outside_trusted_root")
    before = path.lstat()
    if stat.S_ISLNK(before.st_mode) or not stat.S_ISREG(before.st_mode):
        raise RuntimeError("plugin_snapshot_not_regular_file")

    flags = os.O_RDONLY | getattr(os, "O_BINARY", 0)
    if hasattr(os, "O_NOFOLLOW"):
        flags |= os.O_NOFOLLOW
    descriptor = os.open(path, flags)
    try:
        opened = os.fstat(descriptor)
        if (
            opened.st_dev != before.st_dev or opened.st_ino != before.st_ino
            or opened.st_size != before.st_size or opened.st_mtime_ns != before.st_mtime_ns
        ):
            raise RuntimeError("plugin_snapshot_changed_before_read")
        digest = hashlib.sha256()
        while True:
            block = os.read(descriptor, 1024 * 1024)
            if not block:
                break
            digest.update(block)
        after = os.fstat(descriptor)
        if (
            after.st_size != opened.st_size
            or after.st_mtime_ns != opened.st_mtime_ns
        ):
            raise RuntimeError("plugin_snapshot_changed_during_read")
    finally:
        os.close(descriptor)
    current = path.lstat()
    if (
        current.st_dev != before.st_dev or current.st_ino != before.st_ino
        or current.st_size != before.st_size or current.st_mtime_ns != before.st_mtime_ns
    ):
        raise RuntimeError("plugin_snapshot_replaced_after_read")
    return digest.hexdigest()


def evaluate_plugins(policy: PluginPolicy, snapshot: PluginSnapshot) -> dict:
    if (
        not policy.revision or not policy.trusted_snapshot_roots
        or snapshot.phase != "PRE_ACTIVATION"
        or not snapshot.population_complete
        or not snapshot.activation_disabled
        or not snapshot.snapshot_id
    ):
        raise RuntimeError("incomplete_or_unsafe_plugin_snapshot")

    rules: dict[str, PluginRule] = {}
    for rule in policy.rules:
        if (
            not rule.loader_key or rule.loader_key in rules
            or len(rule.sha256) != 64
            or set(rule.sha256) - set("0123456789abcdef")
            or not rule.publisher or not rule.version
        ):
            raise RuntimeError("invalid_or_duplicate_plugin_policy_rule")
        rules[rule.loader_key] = rule

    loader_keys = list(snapshot.loader_population)
    candidates = list(snapshot.candidates)
    candidate_keys = [item.loader_key for item in candidates]
    candidate_paths = [str(Path(item.snapshot_path).resolve(strict=True)) for item in candidates]
    if (
        len(loader_keys) != len(set(loader_keys))
        or len(candidate_keys) != len(set(candidate_keys))
        or len(candidate_paths) != len(set(candidate_paths))
        or set(loader_keys) != set(candidate_keys)
    ):
        raise RuntimeError("loader_population_and_snapshot_do_not_match")

    findings: list[dict[str, str]] = []
    verified: list[dict[str, str]] = []
    for candidate in candidates:
        actual_digest = _hash_regular_snapshot(
            candidate.snapshot_path, policy.trusted_snapshot_roots
        )
        rule = rules.get(candidate.loader_key)
        reason = None
        if rule is None:
            reason = "plugin_not_allowlisted"
        elif candidate.publisher != rule.publisher:
            reason = "publisher_mismatch"
        elif candidate.version != rule.version:
            reason = "version_mismatch"
        elif actual_digest != rule.sha256:
            reason = "artifact_digest_mismatch"
        if reason:
            findings.append({
                "loader_key": candidate.loader_key,
                "reason": reason,
                "actual_sha256": actual_digest,
            })
        else:
            verified.append({
                "loader_key": candidate.loader_key,
                "version": candidate.version,
                "sha256": actual_digest,
            })

    installed = set(candidate_keys)
    for rule in rules.values():
        if rule.required and rule.loader_key not in installed:
            findings.append({
                "loader_key": rule.loader_key,
                "reason": "required_plugin_missing",
                "actual_sha256": "",
            })

    return {
        "schema_version": 1,
        "policy_revision": policy.revision,
        "snapshot_id": snapshot.snapshot_id,
        "population_count": len(candidates),
        "verified": sorted(verified, key=lambda item: item["loader_key"]),
        "findings": sorted(
            findings, key=lambda item: (item["loader_key"], item["reason"])
        ),
        "decision": "PASS" if not findings else "FAIL",
    }


def enforce_plugin_gate(
    policy_provider: VerifiedPolicyProvider,
    inventory: HostPluginInventory,
    evidence_sink: EvidenceSink,
) -> dict:
    policy = policy_provider.load_verified()
    snapshot = inventory.snapshot_before_activation()
    try:
        report = evaluate_plugins(policy, snapshot)
    except Exception:
        inventory.keep_disabled_and_readback(snapshot.loader_population)
        raise
    if report["decision"] != "PASS":
        disable_receipt = inventory.keep_disabled_and_readback(
            tuple(item["loader_key"] for item in report["findings"])
        )
        report["disable_receipt"] = disable_receipt
    evidence_sink.write_and_readback(report)
    if report["decision"] != "PASS":
        raise RuntimeError("plugin_admission_denied")
    return report
Production implementation

Implement HostPluginInventory for the actual client loader: enumerate every loader search root and built-in extension source, reject duplicate identities, snapshot package bytes before imports or hooks execute, and prove activation is disabled. The adapter—not the plugin—extracts loader identity, version, and publisher evidence. For scheduled post-start scans, quarantine or restart to a pre-activation state before declaring enforcement; a scan that cannot reconcile the complete loader population is ERROR, never PASS. Keep signature verification, endpoint management, and fleet quarantine inside the existing policy and endpoint-control adapters.

Independent verification

Replay startup and scheduled scans with an unknown, renamed, duplicated, downgraded, republished, digest-swapped, symlinked, concurrently replaced, hidden search-path, already-loaded, offline, and incomplete-population plugin. Confirm that the loader remains disabled, the evidence receipt covers the exact snapshot and policy revision, and only a complete byte-bound population produces PASS.

Open-source tools

Commercial / hosted tools

Threat mappings

MITRE ATLAS

MAESTRO

OWASP LLM Top 10 2026

OWASP ML Top 10 2023

OWASP Top 10 for Agentic Applications 2026

NIST Adversarial Machine Learning 2025

Cisco Integrated AI Security and Safety Framework

Google Secure AI Framework 2.0 - Risks

Databricks AI Security Framework 3.0

AID-H-028.006: Client Distribution, Plugin & Update Integrity Verification | AIDEFEND

Loading interactive view…

A Practical Knowledge Base for AI Security Defenses

Version