AID-H-024.002: MCP Tool Descriptor Hash Binding & Drift Detection
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.
AID-H-024.002-G001
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
- Cosign
- jq
- python-jsonschema
- OpenTelemetry
- Snyk Agent Scan (descriptor-drift and tool-poisoning checks; Snyk verification API required; sandbox untrusted MCP stdio discovery)
Commercial / hosted tools
- Splunk
- Datadog
- Sumo Logic (log analytics)
Threat mappings
MITRE ATLAS
- AML.T0011 User Execution
- AML.T0110 AI Agent Tool Poisoning
- AML.T0115 Publish Poisoned AI Artifacts
- AML.T0110.000 AI Agent Tool Poisoning: Definition and Instructions
- AML.T0010 AI Supply Chain Compromise (descriptor hash detects compromised tool metadata)
- AML.T0115.002 Publish Poisoned AI Artifacts: AI Agent Tools
- AML.T0011.002 User Execution: Poisoned AI Agent Tool
- AML.T0074 Masquerading (hash binding detects descriptor-level masquerading)
- AML.T0010.005 AI Supply Chain Compromise: AI Agent Tool
- AML.T0109 AI Supply Chain Rug Pull
MAESTRO
- Supply Chain Attacks (Cross-Layer)
- Compromised Agent Registry (L7) (descriptor hash binding detects registry-level descriptor tampering)
OWASP LLM Top 10 2026
- LLM01:2026 Prompt Injection
OWASP ML Top 10 2023
- ML06:2023 AI Supply Chain Attacks (descriptor integrity is part of supply chain security)
OWASP Top 10 for Agentic Applications 2026
- ASI04:2026 Agentic Supply Chain Vulnerabilities
NIST Adversarial Machine Learning 2025
- N/A
Cisco Integrated AI Security and Safety Framework
- AISubtech-12.1.2 Tool Poisoning (pins reviewed descriptor bytes; semantic honesty requires separate review)
- AISubtech-12.1.4 Tool Shadowing (pins reviewed descriptor bytes; semantic honesty requires separate review)
- AISubtech-9.3.3 Dependency Replacement / Rug Pull
- AITech-12.1 Tool Exploitation (pins reviewed descriptor bytes; semantic honesty requires separate review)
- AITech-9.3 Dependency / Plugin Compromise
Google Secure AI Framework 2.0 - Risks
- IIC: Insecure Integrated Component (drift detection identifies compromised tool integrations)
Databricks AI Security Framework 3.0
- Agents - Tools MCP Server 13.18: Tool Poisoning
- Agents - Tools MCP Server 13.21: Supply Chain Attacks