@kya-os/agentshield-nextjs
Version:
Next.js middleware for AgentShield AI agent detection
371 lines (368 loc) • 12.1 kB
JavaScript
;
// src/signature-verifier.ts
var KNOWN_KEYS = {
chatgpt: [
{
kid: "otMqcjr17mGyruktGvJU8oojQTSMHlVm7uO-lrcqbdg",
// ChatGPT's current Ed25519 public key (base64)
publicKey: "7F_3jDlxaquwh291MiACkcS3Opq88NksyHiakzS-Y1g",
validFrom: (/* @__PURE__ */ new Date("2025-01-01")).getTime() / 1e3,
validUntil: (/* @__PURE__ */ new Date("2025-04-11")).getTime() / 1e3
}
]
};
function parseSignatureInput(signatureInput) {
try {
const match = signatureInput.match(/sig1=\((.*?)\);(.+)/);
if (!match) return null;
const [, headersList, params] = match;
const signedHeaders = headersList ? headersList.split(" ").map((h) => h.replace(/"/g, "").trim()).filter((h) => h.length > 0) : [];
const keyidMatch = params ? params.match(/keyid="([^"]+)"/) : null;
const createdMatch = params ? params.match(/created=(\d+)/) : null;
const expiresMatch = params ? params.match(/expires=(\d+)/) : null;
if (!keyidMatch || !keyidMatch[1]) return null;
return {
keyid: keyidMatch[1],
created: createdMatch && createdMatch[1] ? parseInt(createdMatch[1]) : void 0,
expires: expiresMatch && expiresMatch[1] ? parseInt(expiresMatch[1]) : void 0,
signedHeaders
};
} catch (error) {
console.error("[Signature] Failed to parse Signature-Input:", error);
return null;
}
}
function buildSignatureBase(method, path, headers, signedHeaders) {
const components = [];
for (const headerName of signedHeaders) {
let value;
switch (headerName) {
case "@method":
value = method.toUpperCase();
break;
case "@path":
value = path;
break;
case "@authority":
value = headers["host"] || headers["Host"] || "";
break;
default:
const key = Object.keys(headers).find(
(k) => k.toLowerCase() === headerName.toLowerCase()
);
value = key ? headers[key] || "" : "";
break;
}
components.push(`"${headerName}": ${value}`);
}
return components.join("\n");
}
async function verifyEd25519Signature(publicKeyBase64, signatureBase64, message) {
try {
const publicKeyBytes = Uint8Array.from(atob(publicKeyBase64), (c) => c.charCodeAt(0));
const signatureBytes = Uint8Array.from(atob(signatureBase64), (c) => c.charCodeAt(0));
const messageBytes = new TextEncoder().encode(message);
if (publicKeyBytes.length !== 32) {
console.error("[Signature] Invalid public key length:", publicKeyBytes.length);
return false;
}
if (signatureBytes.length !== 64) {
console.error("[Signature] Invalid signature length:", signatureBytes.length);
return false;
}
const publicKey = await crypto.subtle.importKey(
"raw",
publicKeyBytes,
{
name: "Ed25519",
namedCurve: "Ed25519"
},
false,
["verify"]
);
const isValid = await crypto.subtle.verify(
"Ed25519",
publicKey,
signatureBytes,
messageBytes
);
return isValid;
} catch (error) {
console.error("[Signature] Ed25519 verification failed:", error);
if (typeof window === "undefined") {
try {
console.warn("[Signature] Ed25519 not supported in this environment");
return false;
} catch {
return false;
}
}
return false;
}
}
async function verifyAgentSignature(method, path, headers) {
const signature = headers["signature"] || headers["Signature"];
const signatureInput = headers["signature-input"] || headers["Signature-Input"];
const signatureAgent = headers["signature-agent"] || headers["Signature-Agent"];
if (!signature || !signatureInput) {
return {
isValid: false,
confidence: 0,
reason: "No signature headers present",
verificationMethod: "none"
};
}
const parsed = parseSignatureInput(signatureInput);
if (!parsed) {
return {
isValid: false,
confidence: 0,
reason: "Invalid Signature-Input header",
verificationMethod: "none"
};
}
if (parsed.created) {
const now2 = Math.floor(Date.now() / 1e3);
const age = now2 - parsed.created;
if (age > 300) {
return {
isValid: false,
confidence: 0,
reason: "Signature expired (older than 5 minutes)",
verificationMethod: "none"
};
}
if (age < -30) {
return {
isValid: false,
confidence: 0,
reason: "Signature timestamp is in the future",
verificationMethod: "none"
};
}
}
let agent;
let knownKeys;
if (signatureAgent === '"https://chatgpt.com"' || signatureAgent?.includes("chatgpt.com")) {
agent = "ChatGPT";
knownKeys = KNOWN_KEYS.chatgpt;
}
if (!agent || !knownKeys) {
return {
isValid: false,
confidence: 0,
reason: "Unknown signature agent",
verificationMethod: "none"
};
}
const key = knownKeys.find((k) => k.kid === parsed.keyid);
if (!key) {
return {
isValid: false,
confidence: 0,
reason: `Unknown key ID: ${parsed.keyid}`,
verificationMethod: "none"
};
}
const now = Math.floor(Date.now() / 1e3);
if (now < key.validFrom || now > key.validUntil) {
return {
isValid: false,
confidence: 0,
reason: "Key is not valid at current time",
verificationMethod: "none"
};
}
const signatureBase = buildSignatureBase(method, path, headers, parsed.signedHeaders);
let signatureValue = signature;
if (signatureValue.startsWith("sig1=:")) {
signatureValue = signatureValue.substring(6);
}
if (signatureValue.endsWith(":")) {
signatureValue = signatureValue.slice(0, -1);
}
const isValid = await verifyEd25519Signature(
key.publicKey,
signatureValue,
signatureBase
);
if (isValid) {
return {
isValid: true,
agent,
keyid: parsed.keyid,
confidence: 1,
// 100% confidence for valid signature
verificationMethod: "signature"
};
} else {
return {
isValid: false,
confidence: 0,
reason: "Signature verification failed",
verificationMethod: "none"
};
}
}
function hasSignatureHeaders(headers) {
return !!((headers["signature"] || headers["Signature"]) && (headers["signature-input"] || headers["Signature-Input"]));
}
function isChatGPTSignature(headers) {
const signatureAgent = headers["signature-agent"] || headers["Signature-Agent"];
return signatureAgent === '"https://chatgpt.com"' || (signatureAgent?.includes("chatgpt.com") || false);
}
// src/edge-detector-wrapper.ts
var AI_AGENT_PATTERNS = [
{ pattern: /chatgpt-user/i, type: "chatgpt", name: "ChatGPT" },
{ pattern: /claude-web/i, type: "claude", name: "Claude" },
{ pattern: /perplexitybot/i, type: "perplexity", name: "Perplexity" },
{ pattern: /perplexity-user/i, type: "perplexity", name: "Perplexity" },
{ pattern: /perplexity-ai/i, type: "perplexity", name: "Perplexity" },
{ pattern: /perplexity/i, type: "perplexity", name: "Perplexity" },
// Fallback
{ pattern: /bingbot/i, type: "bing", name: "Bing AI" },
{ pattern: /anthropic-ai/i, type: "anthropic", name: "Anthropic" }
];
var CLOUD_PROVIDERS = {
aws: ["54.", "52.", "35.", "18.", "3."],
gcp: ["35.", "34.", "104.", "107.", "108."],
azure: ["13.", "20.", "40.", "52.", "104."]
};
var EdgeAgentDetector = class {
async analyze(input) {
const reasons = [];
let detectedAgent;
let verificationMethod;
let confidence = 0;
const headers = input.headers || {};
const normalizedHeaders = {};
for (const [key, value] of Object.entries(headers)) {
normalizedHeaders[key.toLowerCase()] = value;
}
if (hasSignatureHeaders(headers)) {
try {
const signatureResult = await verifyAgentSignature(
input.method || "GET",
input.url || "/",
headers
);
if (signatureResult.isValid) {
confidence = signatureResult.confidence;
reasons.push(`verified_signature:${signatureResult.agent?.toLowerCase() || "unknown"}`);
if (signatureResult.agent) {
detectedAgent = {
type: signatureResult.agent.toLowerCase(),
name: signatureResult.agent
};
}
verificationMethod = signatureResult.verificationMethod;
if (signatureResult.keyid) {
reasons.push(`keyid:${signatureResult.keyid}`);
}
} else {
confidence = Math.max(confidence, 0.3);
reasons.push("invalid_signature");
if (signatureResult.reason) {
reasons.push(`signature_error:${signatureResult.reason}`);
}
if (isChatGPTSignature(headers)) {
reasons.push("claims_chatgpt");
detectedAgent = { type: "chatgpt", name: "ChatGPT (unverified)" };
}
}
} catch (error) {
console.error("[EdgeAgentDetector] Signature verification error:", error);
confidence = Math.max(confidence, 0.2);
reasons.push("signature_verification_error");
}
}
const userAgent = input.userAgent || input.headers?.["user-agent"] || "";
if (userAgent) {
for (const { pattern, type, name } of AI_AGENT_PATTERNS) {
if (pattern.test(userAgent)) {
const highConfidenceAgents = [
"chatgpt",
"claude",
"perplexity",
"anthropic"
];
const patternConfidence = highConfidenceAgents.includes(type) ? 0.85 : 0.5;
confidence = Math.max(confidence, patternConfidence);
reasons.push(`known_pattern:${type}`);
if (!detectedAgent) {
detectedAgent = { type, name };
verificationMethod = "pattern";
}
break;
}
}
}
const aiHeaders = [
"openai-conversation-id",
"openai-ephemeral-user-id",
"anthropic-client-id",
"x-goog-api-client",
"x-ms-copilot-id"
];
const foundAiHeaders = aiHeaders.filter(
(header) => normalizedHeaders[header]
);
if (foundAiHeaders.length > 0) {
confidence = Math.max(confidence, 0.6);
reasons.push(`ai_headers:${foundAiHeaders.length}`);
}
const ip = input.ip || input.ipAddress;
if (ip && !normalizedHeaders["x-forwarded-for"] && !normalizedHeaders["x-real-ip"]) {
for (const [provider, prefixes] of Object.entries(CLOUD_PROVIDERS)) {
if (prefixes.some((prefix) => ip.startsWith(prefix))) {
confidence = Math.max(confidence, 0.4);
reasons.push(`cloud_provider:${provider}`);
break;
}
}
}
if (reasons.length > 2 && confidence < 1) {
confidence = Math.min(confidence * 1.2, 0.95);
}
return {
isAgent: confidence > 0.3,
confidence,
...detectedAgent && { detectedAgent },
reasons,
...verificationMethod && { verificationMethod },
forgeabilityRisk: verificationMethod === "signature" ? "low" : confidence > 0.8 ? "medium" : "high",
timestamp: /* @__PURE__ */ new Date()
};
}
};
var EdgeAgentDetectorWrapper = class {
detector;
events = /* @__PURE__ */ new Map();
constructor(_config) {
this.detector = new EdgeAgentDetector();
}
async analyze(input) {
const result = await this.detector.analyze(input);
if (result.isAgent && this.events.has("agent.detected")) {
const handlers = this.events.get("agent.detected") || [];
handlers.forEach((handler) => handler(result, input));
}
return result;
}
on(event, handler) {
if (!this.events.has(event)) {
this.events.set(event, []);
}
this.events.get(event).push(handler);
}
emit(event, ...args) {
const handlers = this.events.get(event) || [];
handlers.forEach((handler) => handler(...args));
}
async init() {
return;
}
};
exports.EdgeAgentDetectorWrapper = EdgeAgentDetectorWrapper;
//# sourceMappingURL=edge-detector-wrapper.js.map
//# sourceMappingURL=edge-detector-wrapper.js.map