UNPKG

@kya-os/agentshield-nextjs

Version:

Next.js middleware for AgentShield AI agent detection

220 lines (218 loc) 6.83 kB
'use strict'; // 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); } exports.hasSignatureHeaders = hasSignatureHeaders; exports.isChatGPTSignature = isChatGPTSignature; exports.verifyAgentSignature = verifyAgentSignature; //# sourceMappingURL=signature-verifier.js.map //# sourceMappingURL=signature-verifier.js.map