Blocking MCP Tool Squatting · Rug Pull: Directly Implementing ETDI and OAuth Signature-Based Integrity Verification
With the advent of an era where AI agents can freely invoke external tools, the Model Context Protocol (MCP) ecosystem, which serves as the connecting link, is becoming a new attack surface. MCP is an LLM-tool integration protocol proposed by Anthropic that enables agents to invoke external functions, such as sending emails and reading files, in a standardized manner. In January 2026, credentials and conversation history were leaked from over 2,000 MCP instances via an unauthenticated gateway at Clawdbot, an AI agent gateway service; furthermore, in September 2025, a legitimate npm email package was secretly embedded with logic that BCC all outgoing emails to the attacker's address after building trust across 15 versions. Attacks like these, which involve quietly modifying tool definitions after the fact or stealthily inserting malicious tools with identical names into agents, are difficult to even detect within existing client-side architectures.
This article provides a step-by-step guide on how to directly implement tool signing based on ETDI (Enhanced Tool Definition Interface) and OAuth 2.0 to block Tool Squatting and Rug Pull attacks at the runtime level. You can understand how the three principles of proving the origin and integrity of a tool with cryptographic signatures, detecting post-hoc changes with immutable version control, and declaring least privilege with OAuth scopes work together.
ETDI is an MCP security extension specification officially announced on arXiv (arXiv:2506.01333) in June 2025, and its integration is currently being discussed via MCP Python SDK PR #845. Given the reality that tool poisoning in STRIDE/DREAD threat modeling scored 46.5/50 (Critical) in DREAD and ranked 1st in the OWASP LLM Top 10, it is a very meaningful task to understand this framework and implement it directly starting now.
Key Concepts
Tool Squatting and Rug Pull — Two attacks targeting agents
In the MCP ecosystem, agents can connect to multiple servers simultaneously, and each server can register tools at will. Tool Squatting is an attack that exploits this vulnerability by having a malicious server register tools with names identical to those of legitimate servers (e.g., send_email, read_file). Since LLM non-deterministically calls one of the two, there is a probability that the malicious tool will be executed.
Rug Pull attacks are more insidious. They involve the server secretly modifying the definition of a tool after the fact, even if the user has approved it once. Since most MCP clients validate tools only at the time of installation and do not track subsequent changes, the agent continues to invoke the already weaponized tool, perceiving it as a trusted tool.
Attack Impact: If 50 agents use the same compromised tool, a single supply chain event results in 50 simultaneous infections. A single version update of a package can threaten the entire ecosystem.
Three Core Principles of ETDI
ETDI blocks the two attacks mentioned above simultaneously by combining three principles.
| Principle | Threats Resolved | Mechanism |
|---|---|---|
| Cryptographic Identity Verification | Tool Squatting | Signing tool definitions with ECDSA private key, verifying origin with public key |
| Immutable Version Control | Rug Pull | New version + new signature required upon definition change, re-authorization enforced |
| OAuth 2.0 Explicit Authorization Declaration | Excessive Authorization | Declaring Scopes in Signed Definitions, Runtime Policy Engine Integration |
Important Premise: The public key itself cannot fully prove its origin. For ETDI to be fully effective, a Root of Trust (e.g., an internal organizational trust registry, CA certificate) is required to separately verify that the JWKS endpoint in public_key_ref belongs to a trusted provider. Without this scheme, an attacker could also upload their own public key to public_key_ref and register a tool.
ETDI Tool Definition Structure
The fields version, permissions, provider, signature, and public_key_ref are added to the existing MCP tool definition.
{
"name": "send_email",
"version": "1.2.0",
"description": "지정된 수신자에게 이메일을 발송합니다.",
"schema": {
"input": {
"type": "object",
"properties": {
"to": { "type": "string" },
"subject": { "type": "string" },
"body": { "type": "string" }
},
"required": ["to", "subject", "body"]
},
"output": { "type": "object" }
},
"permissions": ["email:send"],
"provider": "trusted-email-service",
"signature": "eyJhbGciOiJFUzI1NiJ9.eyJuYW1lIjoic2VuZF9lbWFpbCIsInZlcnNpb24iOiIxLjIuMCJ9.MEUCIQD...",
"public_key_ref": "https://keys.trusted-email-service.com/.well-known/jwks.json"
}JWKS(JSON Web Key Set): A set of public keys standardized in JSON format. Clients retrieve the public key from the public_key_ref URL and validate the signature value. Since the URL remains the same even during key rotation, no changes to client code are necessary.
base64url: Unlike standard Base64 (including +, /, =), this is an encoding method that is safe for URLs and HTTP headers (using -, _, padding omitted). The headers, payloads, and signatures of a JWT are all encoded in base64url, which allows them to be inserted directly into URL parameters or JSON fields.
Signature Generation and Verification Flow
sequenceDiagram
participant S as MCP 서버
participant JW as JWKS 엔드포인트
participant C as MCP 클라이언트
S->>S: 도구 정의 생성 + ES256 JWT 서명
S->>JW: 공개 키 게시(/.well-known/jwks.json)
C->>S: 도구 목록 조회
S->>C: 서명된 도구 정의 반환
C->>JW: 공개 키 조회 (TTL 캐싱)
JW->>C: JWKS 응답
C->>C: JWT 서명 검증
alt 검증 성공
C->>C: 버전·스코프 변경 확인
C->>S: 도구 호출
else 검증 실패
C->>C: 도구 호출 거부
endPractical Application
Example 1: Implementing an ETDI Tool Signing Server with Python
You can configure a server that applies ECDSA ES256 JWT signatures to tool definitions using the cryptography and PyJWT libraries. Using the JWT format enables cross-verification with all standard JOSE libraries, such as Python, TypeScript, and Java, and the library also internally handles the DER/P1363 encoding mismatch issue that will be explained later.
# 의존성 설치
uv add cryptography PyJWT# etdi_server.py
import json
import base64
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.backends import default_backend
import jwt # PyJWT
class ETDIToolSigner:
"""도구 정의에 ECDSA P-256 JWT 서명을 생성하는 클래스"""
def __init__(self):
# 프로덕션에서는 HSM 또는 Secret Manager에서 키를 로드합니다
self._private_key = ec.generate_private_key(
ec.SECP256R1(), default_backend()
# SECP256R1 = P-256: NIST 표준 곡선으로 성능·보안·호환성의
# 균형이 잡혀 있으며 JOSE 생태계 전반에서 지원됩니다
)
self._public_key = self._private_key.public_key()
def sign_tool_definition(self, tool_def: dict) -> dict:
"""
도구 정의의 핵심 필드를 JWT 페이로드로 서명합니다.
서명 대상: name + version + schema + permissions + provider
"""
payload = {
"name": tool_def["name"],
"version": tool_def["version"],
"schema": tool_def["schema"],
# sorted()로 권한 목록 정렬 — 결정론적 직렬화가 핵심입니다
"permissions": sorted(tool_def.get("permissions", [])),
"provider": tool_def["provider"],
}
# PyJWT가 ES256(ECDSA + SHA-256) JWT를 생성합니다
# jose(TypeScript) 등 표준 JOSE 라이브러리로 교차 검증이 가능합니다
tool_def["signature"] = jwt.encode(
payload, self._private_key, algorithm="ES256"
)
return tool_def
def export_jwks(self) -> dict:
"""클라이언트가 서명 검증에 사용할 JWKS를 반환합니다."""
# _private_key.public_key()로 분리한 공개 키 객체에서 좌표를 추출합니다
pub_numbers = self._public_key.public_numbers()
x = base64.urlsafe_b64encode(
pub_numbers.x.to_bytes(32, 'big')
).rstrip(b'=').decode()
y = base64.urlsafe_b64encode(
pub_numbers.y.to_bytes(32, 'big')
).rstrip(b'=').decode()
return {
"keys": [{
"kty": "EC",
"crv": "P-256",
"use": "sig",
"alg": "ES256",
"kid": "etdi-key-v1",
"x": x,
"y": y,
}]
}
# 도구 정의 예시
signer = ETDIToolSigner()
send_email_tool = {
"name": "send_email",
"version": "1.2.0",
"description": "지정된 수신자에게 이메일을 발송합니다.",
"schema": {
"input": {
"type": "object",
"properties": {
"to": {"type": "string", "format": "email"},
"subject": {"type": "string"},
"body": {"type": "string"}
},
"required": ["to", "subject", "body"]
}
},
"permissions": ["email:send"],
"provider": "trusted-email-service",
"public_key_ref": "https://keys.trusted-email-service.com/.well-known/jwks.json"
}
signed_tool = signer.sign_tool_definition(send_email_tool)
print(json.dumps(signed_tool, indent=2, ensure_ascii=False))
jwks = signer.export_jwks()
print(json.dumps(jwks, indent=2))| Code Point | Description |
|---|---|
ec.SECP256R1() |
P-256 Curve Selection — NIST standard and supported across the JOSE ecosystem |
sorted(permissions) |
Makes the serialization result deterministic by sorting the permission list |
jwt.encode(..., algorithm="ES256") |
JWT format signature — DER/P1363 cross-verification is possible without encoding mismatch |
| Use the public key object pre-separated into | self._public_key.public_numbers() |
export_jwks() |
/.well-known/jwks.json endpoint returns this response |
Example 2: Verifying Signatures on a TypeScript MCP Client
You can cleanly implement JWKS lookup and JWT validation using only the jose library.
# 의존성 설치
pnpm add jose// etdi-verifier.ts
import { createRemoteJWKSet, jwtVerify } from 'jose';
interface ETDIToolDefinition {
name: string;
version: string;
schema: Record<string, unknown>;
permissions: string[];
provider: string;
signature: string; // ES256 JWT
public_key_ref: string; // JWKS URL
}
interface VerificationResult {
valid: boolean;
reason?: string;
}
// createRemoteJWKSet은 내부적으로 키를 캐싱합니다
// 추가 TTL 제어가 필요하면 cooldownThreshold 옵션을 사용합니다
const jwksCache = new Map<string, ReturnType<typeof createRemoteJWKSet>>();
function getJWKS(jwksUrl: string) {
if (!jwksCache.has(jwksUrl)) {
jwksCache.set(jwksUrl, createRemoteJWKSet(new URL(jwksUrl)));
}
return jwksCache.get(jwksUrl)!;
}
/**
* ETDI 도구 정의의 JWT 서명을 검증합니다.
* 검증 실패 시 도구 호출을 거부해야 합니다.
*/
async function verifyETDITool(tool: ETDIToolDefinition): Promise<VerificationResult> {
try {
const JWKS = getJWKS(tool.public_key_ref);
// jose가 JWKS 조회 + ES256 서명 검증을 한 번에 처리합니다
const { payload } = await jwtVerify(tool.signature, JWKS, {
algorithms: ['ES256'],
});
// JWT가 유효해도 서명된 내용과 실제 정의가 다를 수 있으므로
// 핵심 필드를 직접 비교합니다
const permissionsMatch =
JSON.stringify([...tool.permissions].sort()) ===
JSON.stringify([...((payload.permissions as string[]) ?? [])].sort());
if (
payload.name !== tool.name ||
payload.version !== tool.version ||
payload.provider !== tool.provider ||
!permissionsMatch
) {
return {
valid: false,
reason: '서명 페이로드와 도구 정의가 불일치합니다 — 변조 의심.',
};
}
return { valid: true };
} catch (error) {
return {
valid: false,
reason: `검증 오류: ${error instanceof Error ? error.message : '알 수 없는 오류'}`,
};
}
}
/**
* 버전 변경을 감지하여 재승인 여부를 판단합니다.
* 승인된 버전은 로컬 SQLite나 JSON 파일에 저장합니다.
*/
function hasVersionChanged(approvedVersion: string, currentVersion: string): boolean {
return approvedVersion !== currentVersion;
}
// 도구 발견 시 호출되는 핸들러
async function onToolDiscovered(tool: ETDIToolDefinition) {
const result = await verifyETDITool(tool);
if (!result.valid) {
console.error(`[ETDI] 도구 "${tool.name}" 차단: ${result.reason}`);
return; // 도구 등록 거부 — 에이전트에 노출되지 않습니다
}
const approvedVersion = getApprovedVersion(tool.name, tool.provider);
if (approvedVersion && hasVersionChanged(approvedVersion, tool.version)) {
console.warn(
`[ETDI] 도구 "${tool.name}" 버전 변경 감지: ${approvedVersion} → ${tool.version}`
);
await promptUserReapproval(tool);
return;
}
console.log(`[ETDI] 도구 "${tool.name}" v${tool.version} 서명 검증 완료`);
registerTool(tool);
}
// 실제 구현에서는 별도 모듈에 위치합니다
declare function getApprovedVersion(name: string, provider: string): string | null;
declare function promptUserReapproval(tool: ETDIToolDefinition): Promise<void>;
declare function registerTool(tool: ETDIToolDefinition): void;| Code Point | Description |
|---|---|
createRemoteJWKSet |
Handles JWKS lookups and key caching internally |
jwtVerify |
Handles signature verification + algorithm whitelist check at once |
| Payload Field Comparison | Direct comparison is performed because even if the signature is valid, the signed content may differ from the actual definition. |
hasVersionChanged() |
Extending with SemVer parsing allows you to distinguish patch/minor/major changes |
Example 3: OAuth 2.0 Scope-Based Runtime Access Control
This is a middleware pattern that compares the scope of the actual OAuth token with the permissions declared in the tool definition at runtime. It must perform both token signature verification and scope checking.
# 의존성 설치
uv add cryptography PyJWT requests# oauth_scope_guard.py
from functools import wraps
from typing import Callable, Any
import jwt
from jwt.algorithms import ECAlgorithm
import requests
class OAuthScopeGuard:
"""
도구 호출 전 OAuth 토큰의 서명을 JWKS 공개 키로 검증하고,
ETDI 도구 정의의 permissions를 충족하는지 확인합니다.
"""
def __init__(self, jwks_url: str):
self.jwks_url = jwks_url
self._cached_public_key = None
def _fetch_public_key(self) -> Any:
"""JWKS 엔드포인트에서 EC 공개 키를 가져옵니다 (인메모리 캐싱)."""
if self._cached_public_key:
return self._cached_public_key
resp = requests.get(self.jwks_url, timeout=5)
resp.raise_for_status()
keys = resp.json().get("keys", [])
ec_key = next((k for k in keys if k.get("kty") == "EC"), None)
if not ec_key:
raise ValueError("JWKS에 EC 키가 없습니다.")
# PyJWT 내장 변환 함수로 JWKS EC 키를 cryptography 공개 키 객체로 변환합니다
self._cached_public_key = ECAlgorithm.from_jwk(ec_key)
return self._cached_public_key
def require_scopes(self, required_scopes: list[str]):
"""도구 핸들러에 적용하는 데코레이터"""
def decorator(func: Callable) -> Callable:
@wraps(func)
async def wrapper(*args, request_context: dict, **kwargs) -> Any:
token = request_context.get("oauth_token")
if not token:
raise PermissionError("OAuth 토큰이 없습니다. 인증이 필요합니다.")
# 1단계: JWKS 공개 키로 토큰 서명을 검증합니다
# verify_signature: False는 절대 사용하지 않습니다
try:
public_key = self._fetch_public_key()
claims = jwt.decode(
token,
public_key,
algorithms=["ES256"],
)
except jwt.ExpiredSignatureError:
raise PermissionError("토큰이 만료됐습니다.")
except jwt.InvalidTokenError as e:
raise PermissionError(f"유효하지 않은 토큰: {e}")
# 2단계: 토큰 스코프와 도구 정의 permissions를 비교합니다
token_scopes = set(claims.get("scope", "").split())
missing_scopes = set(required_scopes) - token_scopes
if missing_scopes:
raise PermissionError(
f"권한 부족. 필요한 스코프: {missing_scopes}. "
f"보유한 스코프: {token_scopes}"
)
return await func(*args, request_context=request_context, **kwargs)
return wrapper
return decorator
# MCP 도구 핸들러에 적용 예시
guard = OAuthScopeGuard(jwks_url="https://auth.example.com/.well-known/jwks.json")
@guard.require_scopes(["email:send"])
async def handle_send_email(
to: str,
subject: str,
body: str,
*,
request_context: dict, # 키워드 전용 인자로 위치 인자와의 혼동을 방지합니다
) -> dict:
"""
ETDI 정의의 permissions: ["email:send"]와
런타임 OAuth 토큰의 scope가 일치해야만 실행됩니다.
"""
return {"status": "sent", "to": to}| Code Point | Description |
|---|---|
_fetch_public_key() |
Retrieves the EC public key from JWKS and caches it in memory. The cache must be cleared during key rotation. |
ECAlgorithm.from_jwk() |
Converts a JWKS EC key to a cryptography object using PyJWT built-in conversion functions |
jwt.decode(..., algorithms=["ES256"]) |
Extracts claims after verifying the signature. verify_signature: False is not used |
*, request_context: dict |
Declare as keyword-only arguments to clarify the handler signature |
Pros and Cons Analysis
Advantages
| Item | Content |
|---|---|
| Block Supply Chain Attacks | Instantly detect tool-defined tampering at runtime with signature verification |
| Block Tool Squatting at the Source | Provides a basis for trust between identically named tools by proving the tool issuer using the provider's public key |
| Prevent Rug Pulls | Detects retroactive changes with immutable version control and enforces re-approval upon changes |
| Granular authorization control | Implements the principle of least privilege with OAuth scopes + policy engine |
| Audit Trail | All tool definitions and version history are maintained in a verifiable form |
Disadvantages and Precautions
| Item | Content | Response Plan |
|---|---|---|
| Key Management Infrastructure Requirements | Infrastructure for JWKS endpoints, key rotation, and revocation is required | Utilizing managed services such as Auth0 and WorkOS can reduce operational burden |
| Adoption by the entire ecosystem is required | If adopted partially, vulnerabilities on unapplied servers will remain | Apply to in-house operational servers first, and use ETDI support as the filtering criterion for external servers |
| Pre-production adoption phase | SDK integration is limited as it is not yet included in the MCP main specification | You can track MCP SDK PR #845 and implement it with your own middleware first |
| Performance Overhead | Signature verification is required for every request | Cache JWKS in memory (TTL 1 hour recommended) and perform signature verification only once upon tool registration |
| Absence of Root of Trust | Public keys alone cannot fully prove origin | Enhance the trustworthiness of JWKS endpoints with a Trust Registry or CA Certificate |
Key Rotation: A security practice of periodically rotating encryption keys. In ETDI, when rotating a key, all tool definitions must be re-signed with the new key and the JWKS endpoint updated. If the client caches the JWKS URL, the new key is automatically fetched after the cache TTL.
The Most Common Mistakes in Practice
- Serialization Mismatch: If the server signs
permissionsaligned tosorted(), but the client reconstructs the payload in its original order, even a valid signature will fail. It is important to document the serialization method for the signature and ensure that both sides implement it identically. Using JWT-based signatures, as in the example, allows you to delegate payload serialization to the standard library, which can reduce errors. - DER ↔ IEEE P1363 Encoding Mismatch: The raw ECDSA signatures from the Python
cryptographylibrary use DER encoding, but the browser's Web Crypto API (crypto.subtle) expects the IEEE P1363 (rawr ∥ s) format. This is the most common obstacle encountered when signing with a Python server and verifying directly in the browser. If you generate an ES256 JWT withPyJWT, this conversion is handled internally by the library. If you need to exchange raw ECDSA signatures directly, you must convert them to P1363 format on the server (32-byte big-endian concatenation forrandsrespectively) before transmitting them. - Use PLACEHOLDER_51__: If you skip signature verification while trying to quickly extract only the scope from the token, an attacker can gain full privileges with a forged token with an arbitrary scope. You must verify the signature with a JWKS public key before trusting the claims.
- Skip re-signing by only changing the version: You must generate a new signature whenever any field in the tool definition is changed. You can prevent this mistake by automating the process by including a signature generation step in your CI/CD pipeline.
In Conclusion
An MCP client without signature verification is like a web server without HTTPS. The connection itself works, but there is no way to know who the other party is or if the transmitted content has been tampered with. ETDI is an attempt to introduce the first cryptographic trust layer into the MCP ecosystem, and implementing it now allows you to establish a preemptive security foundation when the ecosystem matures.
3 Steps to Start Right Now:
- Adding ETDI signatures to your MCP server: Install the dependency using
uv add cryptography PyJWT, and refer to theETDIToolSignerclass in the example code to applysign_tool_definition()to your existing tool definition. Also expose the/.well-known/jwks.jsonendpoint. Verify operation: Paste the generated JWT intojwt.ioand verify the signature yourself using thexandyvalues returned byexport_jwks()to check the results. - Writing Client-Side Validation Middleware: Based on the
verifyETDITool()function from the TypeScript example followingpnpm add jose, integrate signature verification and version change detection at the time of tool registration (onToolDiscovered). If you store approved versions in a local SQLite or JSON file, you can start detecting Rug Pulls immediately. It is recommended to verify both a signed tool and a tool with intentionally modifiedversionfields to check the difference in rejection/pass results. - Reviewing the Parallel Application of Trail of Bits’
mcp-context-protector: If ETDI is a fundamental solution requiring key management infrastructure,mcp-context-protectoris a complementary tool that provides immediate Trust-on-first-use pinning and prompt injection scanning without separate infrastructure. Applying both together increases the depth of defense.
Next Post: Building a Dynamic MCP Tool Access Control Policy Engine That Reflects Runtime Context by Integrating OPA (Open Policy Agent) and ETDI
Reference Materials
Sources cited in the text
- ETDI 논문 — Mitigating Tool Squatting and Rug Pull Attacks | arXiv
- ETDI Paper HTML Full Text | arXiv
- MCP Python SDK PR #845 — ETDI | GitHub
- vineethsai/MCP-ETDI-docs — ETDI Official Documentation | GitHub
- We Built the Security Layer MCP Always Needed | Trail of Bits Blog
- trailofbits/mcp-context-protector | GitHub
- MCP Supply Chain Attack Real-World Case Study | Securelist
- Detailed Explanation of MCP Rug Pull Attack | Waxell
Advanced Learning Materials
- ETDI Security Framework | vulnerablemcp.info
- MCP Official Security Best Practices | modelcontextprotocol.io
- MCP Authentication/Authorization Official Tutorial | modelcontextprotocol.io
- MCP Tool Attack Vectors and Defenses | Elastic Security Labs
- MCP Security Vulnerability Timeline | authzed
- Protecting MCP Tools with OAuth and JWT | cloudnativedeepdive
- Analysis of June 2025 MCP Specification Update | Auth0
- MCP Server OAuth 2.1 Implementation Guide | Scalekit
- MCPSecBench — MCP Security Benchmark | arXiv
- MCP Threat Modeling Research | arXiv