AID-H-024.002: MCP Tool Descriptor Hash Binding & Drift Detection

Version 1.20261015 · Actionable sub-technique

Pin MCP/tool descriptors by content hash and fail closed on drift at resolution time.

Pillars: infra, app

Lifecycle: building, operation

Scope boundary

Owns content-hash binding and drift detection for MCP and tool descriptors at consuming-side resolution. It does not govern agent capability manifests or approve the semantics expressed by unchanged descriptor bytes.

AID-H-024.001: AID-H-024.002 verifies descriptor-byte continuity; AID-H-024.001 pins the tool namespace and version used to locate those bytes. Both bindings are required because identity and content can drift independently.

AID-H-024.004: AID-H-024.002 proves that descriptor bytes match an approved digest; AID-H-024.004 decides whether the descriptor's operation, side effects, privilege, and destructive semantics are acceptable. Byte integrity preserves meaning but does not judge it.

Implementation guidance

AID-H-024.002-G001

Canonicalize descriptor JSON, compare it with a pinned manifest at install and resolution time, and fail closed with a structured drift event on mismatch.

Apply this control whenever an MCP or agent tool descriptor is approved by content digest. The descriptor loader supplies the exact bounded response bytes; the pin loader supplies a signature-verified manifest and its receipt. Do not read an unsigned mutable JSON file into a module-level global. The same JSON Canonicalization Scheme implementation and version must be used when pins are created and verified.

Verify the exact descriptor before resolution
# File: tool_resolution/descriptor_integrity.py
from __future__ import annotations

import hashlib
import json
import re
from dataclasses import dataclass

import rfc8785

SHA256 = re.compile(r"^[0-9a-f]{64}$")
MAX_DESCRIPTOR_BYTES = 1_048_576


class DescriptorIntegrityDenied(RuntimeError):
    pass


@dataclass(frozen=True)
class DescriptorPin:
    tool_fqid: str
    canonical_sha256: str


@dataclass(frozen=True)
class VerifiedPinManifest:
    records: dict[str, DescriptorPin]
    policy_version: str
    manifest_sha256: str
    signature_sha256: str
    canonicalizer: str


def unique_object(pairs: list[tuple[str, object]]) -> dict:
    result: dict = {}
    for key, value in pairs:
        if key in result:
            raise DescriptorIntegrityDenied("duplicate_descriptor_key")
        result[key] = value
    return result


def parse_descriptor(raw: bytes) -> dict:
    if not isinstance(raw, bytes) or not 2 <= len(raw) <= MAX_DESCRIPTOR_BYTES:
        raise DescriptorIntegrityDenied("descriptor_size_invalid")
    try:
        parsed = json.loads(
            raw.decode("utf-8", errors="strict"),
            object_pairs_hook=unique_object,
            parse_constant=lambda value: (_ for _ in ()).throw(
                DescriptorIntegrityDenied("non_finite_json_number")
            ),
        )
    except (UnicodeDecodeError, json.JSONDecodeError) as exc:
        raise DescriptorIntegrityDenied("descriptor_not_strict_json") from exc
    if not isinstance(parsed, dict):
        raise DescriptorIntegrityDenied("descriptor_root_must_be_object")
    return parsed


def verify_descriptor(
    tool_fqid: str,
    raw_descriptor: bytes,
    pins: VerifiedPinManifest,
) -> tuple[dict, dict]:
    if not isinstance(pins, VerifiedPinManifest):
        raise DescriptorIntegrityDenied("verified_pin_manifest_required")
    if pins.canonicalizer != "RFC8785":
        raise DescriptorIntegrityDenied("canonicalizer_profile_mismatch")
    if not SHA256.fullmatch(pins.manifest_sha256):
        raise DescriptorIntegrityDenied("invalid_pin_manifest_digest")
    if not SHA256.fullmatch(pins.signature_sha256):
        raise DescriptorIntegrityDenied("invalid_pin_signature_digest")

    pin = pins.records.get(tool_fqid)
    if pin is None:
        raise DescriptorIntegrityDenied("descriptor_pin_missing")
    if not isinstance(pin, DescriptorPin) or pin.tool_fqid != tool_fqid:
        raise DescriptorIntegrityDenied("descriptor_pin_identity_mismatch")
    if not SHA256.fullmatch(pin.canonical_sha256):
        raise DescriptorIntegrityDenied("descriptor_pin_digest_invalid")

    descriptor = parse_descriptor(raw_descriptor)
    try:
        canonical = rfc8785.dumps(descriptor)
    except (TypeError, ValueError) as exc:
        raise DescriptorIntegrityDenied("descriptor_not_canonicalizable") from exc
    actual = hashlib.sha256(canonical).hexdigest()
    if actual != pin.canonical_sha256:
        raise DescriptorIntegrityDenied("descriptor_hash_mismatch")

    receipt = {
        "schema_version": "aidefend.h024002-descriptor.v1",
        "status": "PASS",
        "tool_fqid": tool_fqid,
        "descriptor_raw_sha256": hashlib.sha256(raw_descriptor).hexdigest(),
        "descriptor_canonical_sha256": actual,
        "pin_manifest_sha256": pins.manifest_sha256,
        "pin_signature_sha256": pins.signature_sha256,
        "policy_version": pins.policy_version,
        "canonicalizer": pins.canonicalizer,
    }
    return descriptor, receipt
Enforcement, drift, and evidence

Run the verifier once during startup for the full configured tool population and again on every dynamic descriptor refresh or resolution. Missing pins, signature failure, unsupported canonicalizer profiles, duplicate JSON keys, invalid JSON, and mismatched hashes all block the descriptor. Emit a structured drift receipt for denied outcomes without logging descriptor secrets. A separate read-only job fetches the same descriptor release, verifies the same signed pin manifest, reproduces the canonical digest, and confirms the dispatcher consumed only descriptor receipts from that release.

Action: Replace unsigned mutable PINS globals with a signature-verified pin-manifest dependency, canonicalize the exact bounded descriptor bytes using RFC 8785, and prevent startup, refresh, or tool resolution unless the local descriptor receipt is 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-024.002: MCP Tool Descriptor Hash Binding & Drift Detection | AIDEFEND

Loading interactive view…

A Practical Knowledge Base for AI Security Defenses

Version