Web Bot Auth 是一种基于 HTTP 消息签名的 Agent 身份认证机制,允许 Agent 通过数字签名证明其身份,服务端通过 Agent 请求所携带的签名信息认证 Agent 身份。本示例提供了在边缘函数部署 Web Bot Auth 认证的解决方案,严格遵循 IETF 相关标准草案。
示例代码
// ==================== 配置区域 ====================// 密钥目录获取配置const KEY_DIRECTORY_CONFIG = {// 是否启用缓存(默认启用)enableCache: true,// 是否遵循源站的 Cache-Control 头(默认遵循)respectCacheControl: true,// 请求超时时间,单位:毫秒(默认5秒)timeout: 5000,// 默认的密钥目录路径defaultPath: '/.well-known/http-message-signatures-directory'};// ==================== 常量 ====================// HTTP 消息签名目录的媒体类型(RFC 9421)const MEDIA_TYPE_DIRECTORY = 'application/http-message-signatures-directory+json';// ==================== 边缘函数入口 ====================addEventListener('fetch', (event) => {handleRequest(event);});async function handleRequest(event) {const request = event.request;try {// 所有路径均需要进行签名验证const status = await verifySignature(request, event);if (status === 'valid') {// 签名有效,转发到源站return;} else {// 拒绝无签名或无效签名的请求const errorMessage = status === 'neutral'? 'Missing signature headers': status.replace('invalid: ', '');return event.respondWith(new Response(JSON.stringify({error: 'authentication_failed',message: errorMessage,timestamp: new Date().toISOString()}), {status: 401,headers: {'Content-Type': 'application/json','Cache-Control': 'no-cache, no-store, must-revalidate'}}));}} catch (error) {console.error(`[handleRequest] ${error.stack}`);return event.respondWith(new Response(JSON.stringify({error: 'internal_server_error',message: error.message || 'An internal error occurred',timestamp: new Date().toISOString()}), {status: 500,headers: { 'Content-Type': 'application/json' }}));}}// ==================== 签名验证核心逻辑 ====================/*** 验证请求签名* @param {Request} request - 请求对象* @returns {Promise<string>} 验证状态: 'valid' | 'neutral' | 'invalid: <reason>'*/async function verifySignature(request, event) {try {// 无签名头,返回 'neutral'if (!request.headers.has('Signature')) {return 'neutral';}// 获取签名请求头const signature = request.headers.get('Signature');const signatureInput = request.headers.get('Signature-Input');if (!signature || !signatureInput) {return 'invalid: Missing signature headers';}// 解析 Signature-Input 请求头const parsedSignatureInput = parseSignatureInput(signatureInput);if (!!parsedSignatureInput.error) {return `invalid: ${parsedSignatureInput.error}`;}// 解析 Signature 请求头const parsedSignature = parseSignature(signature, parsedSignatureInput.signatureId);if (!!parsedSignature.error) {return `invalid: ${parsedSignature.error}`;};const signatureBytes = parsedSignature.bytes;// 验证参数const signatureParamsValidation = validateSignatureParams(parsedSignatureInput.params);if (!!signatureParamsValidation.error) {return `invalid: ${signatureParamsValidation.error}`;}// 获取密钥目录 URLconst keyDirectoryUrlResult = getKeyDirectoryUrl(request);if (keyDirectoryUrlResult.error) {return `invalid: ${keyDirectoryUrlResult.error}`;}const keyDirectoryUrl = keyDirectoryUrlResult.url;// 获取密钥目录数据,传入预期 keyid 进行一致性验证const keyDirectoryResult = await fetchKeyDirectory(keyDirectoryUrl, event, parsedSignatureInput.params.keyid);if (keyDirectoryResult.error) {return `invalid: ${keyDirectoryResult.error}`;}const keyDirectory = keyDirectoryResult.directory;const verifiedKeyId = keyDirectoryResult.verifiedKeyId;// 查找公钥(使用已验证的 keyid)const jwk = findPublicKey(keyDirectory.keys, verifiedKeyId);if (!jwk) {return `invalid: Public key not found for keyid: ${verifiedKeyId}`;}// 构建签名基础字符串const signatureBase = buildSignatureBase(request, parsedSignatureInput.components, parsedSignatureInput.signatureParams);console.log(`[verifySignature] signatureBase: ${signatureBase}`);// 验证 Ed25519 签名const [isValid, reason] = await verifyEd25519(signatureBase, signatureBytes, jwk);if (isValid) {return 'valid';} else {return !!reason ? `invalid: ${reason}` : 'invalid: Signature verification failed';}} catch (error) {console.error(`[verifySignature] ${error.stack}`);return `invalid: ${error.message}`;}}/*** 从请求头中提取密钥目录URL* @param {Request} request - 请求对象* @returns {{ error?: string; url?: string }} 解析结果*/function getKeyDirectoryUrl(request) {try {const signatureAgent = request.headers.get('Signature-Agent');if (!signatureAgent) {return { error: 'Missing Signature-Agent header' };}// 值必须用双引号包围if (!signatureAgent.startsWith('"') || !signatureAgent.endsWith('"')) {return { error: 'Signature-Agent header value must be enclosed in double quotes' };}// 去除双引号const urlValue = signatureAgent.slice(1, -1);// 必须是 HTTPS 协议if (!urlValue.startsWith('https://')) {return { error: 'Signature-Agent header value must be a valid HTTPS URL' };}// 验证URL格式let url;try {url = new URL(urlValue);} catch (error) {return { error: `Invalid URL in Signature-Agent header: ${error.message}` };}// 确保 URL 指向正确的密钥目录路径// 如果 URL 不以默认路径结尾,自动添加if (!url.pathname.endsWith(KEY_DIRECTORY_CONFIG.defaultPath)) {// 如果路径不是默认路径,检查是否是域名根路径if (url.pathname === '/' || url.pathname === '') {url.pathname = KEY_DIRECTORY_CONFIG.defaultPath;} else {// 否则保持原始路径,但记录警告console.warn(`[getKeyDirectoryUrl] URL path may not be standard key directory: ${url.pathname}`);}}return { url: url.toString() };} catch (error) {console.error(`[getKeyDirectoryUrl] ${error.stack}`);return { error: `Failed to extract key directory URL: ${error.message}` };}}/*** 获取密钥目录数据,支持缓存* @param {string} url - 密钥目录URL* @param {any} event - 事件对象,用于缓存写入* @param {string|null} expectedKeyId - 预期的keyid,用于验证响应一致性(可选)* @returns {Promise<{ error?: string; directory?: { keys: Array }, verifiedKeyId?: string }>} 获取结果,包含已验证的keyid*/async function fetchKeyDirectory(url, event, expectedKeyId = null) {try {const cache = caches.default;const cacheKey = new Request(url, { eo: { cacheKey: url } });// 如果启用缓存,先尝试从缓存读取if (KEY_DIRECTORY_CONFIG.enableCache) {const cachedResponse = await cache.match(cacheKey);if (cachedResponse) {console.log(`[fetchKeyDirectory] Cache hit for ${url}`);try {// 验证缓存响应是否包含必要的 Signature-Input 头const cachedSignatureInput = cachedResponse.headers.get('Signature-Input');if (!cachedSignatureInput) {console.warn(`[fetchKeyDirectory] Cached response missing Signature-Input header, skipping cache`);// 跳过缓存,继续网络获取} else {// 解析响应头中的 keyid 参数const parsedCachedInput = parseSignatureInput(cachedSignatureInput);if (parsedCachedInput.error) {console.warn(`[fetchKeyDirectory] Failed to parse cached Signature-Input header: ${parsedCachedInput.error}, skipping cache`);} else {const cachedKeyId = parsedCachedInput.params.keyid;if (!cachedKeyId) {console.warn(`[fetchKeyDirectory] Cached response missing keyid parameter, skipping cache`);} else if (expectedKeyId !== null && cachedKeyId !== expectedKeyId) {console.warn(`[fetchKeyDirectory] Cached response keyid mismatch: expected ${expectedKeyId}, got ${cachedKeyId}, skipping cache`);} else {// 缓存验证通过,返回目录和已验证的 keyidconst directory = await cachedResponse.json();return { directory, verifiedKeyId: cachedKeyId };}}}} catch (error) {console.error(`[fetchKeyDirectory] Failed to process cached response: ${error.message}`);// 缓存处理失败,继续网络获取}}}// 构造请求选项,设置超时const fetchOptions = {redirect: 'manual',eo: {timeoutSetting: {connectTimeout: KEY_DIRECTORY_CONFIG.timeout,readTimeout: KEY_DIRECTORY_CONFIG.timeout,writeTimeout: KEY_DIRECTORY_CONFIG.timeout}}};// 发起请求const response = await fetch(url, fetchOptions);if (!response.ok) {return { error: `HTTP ${response.status} fetching key directory from ${url}` };}// 验证 Content-Typeconst contentType = response.headers.get('Content-Type');if (!contentType || !contentType.includes(MEDIA_TYPE_DIRECTORY)) {return { error: `Invalid Content-Type: ${contentType}, expected ${MEDIA_TYPE_DIRECTORY}` };}// 验证 Signature-Input 响应头const responseSignatureInput = response.headers.get('Signature-Input');if (!responseSignatureInput) {return { error: 'Key directory response missing required Signature-Input header' };}// 解析响应头中的 keyid 参数const parsedResponseInput = parseSignatureInput(responseSignatureInput);if (parsedResponseInput.error) {return { error: `Failed to parse response Signature-Input header: ${parsedResponseInput.error}` };}const responseKeyId = parsedResponseInput.params.keyid;if (!responseKeyId) {return { error: 'Key directory response Signature-Input missing keyid parameter' };}// 如果提供了预期 keyid,验证一致性if (expectedKeyId !== null && responseKeyId !== expectedKeyId) {return { error: `Key directory response keyid mismatch: expected ${expectedKeyId}, got ${responseKeyId}` };}// 解析响应体let directory;try {if (KEY_DIRECTORY_CONFIG.enableCache) {directory = await response.clone().json();} else {directory = await response.json();}} catch (error) {return { error: `Failed to parse key directory response as JSON: ${error.message}` };}// 验证目录结构if (!directory || !Array.isArray(directory.keys)) {return { error: 'Invalid key directory structure: missing or invalid "keys" array' };}// 如果启用缓存且响应可缓存,写入缓存if (KEY_DIRECTORY_CONFIG.enableCache) {// 检查是否需要遵循源站 Cache-Controllet shouldCache = true;if (KEY_DIRECTORY_CONFIG.respectCacheControl) {const cacheControl = response.headers.get('Cache-Control');if (cacheControl && cacheControl.includes('no-store')) {shouldCache = false;}}if (shouldCache) {event.waitUntil(cache.put(cacheKey, response.clone()));console.log(`[fetchKeyDirectory] Cached response for ${url}`);}}return { directory, verifiedKeyId: responseKeyId };} catch (error) {console.error(`[fetchKeyDirectory] Error fetching ${url}:`, error);return { error: `Failed to fetch key directory: ${error.message}` };}}/*** 解析 Signature-Input 请求头* @param {string} signatureInput - Signature-Input 请求头值* @returns {{ error?: string; signatureId: string; components: string[]; params: Record<string, string|number>; signatureParams: string;}} 解析结果*/function parseSignatureInput(signatureInput) {try {const match = signatureInput.match(/^(\\w+)=(.+)$/);if (!match) {return { error: 'Failed to parse Signature-Input - invalid Signature-Input format' };}const [, signatureId, paramString] = match;// 解析组件列表const componentsMatch = paramString.match(/^\\(([^)]+)\\)/);if (!componentsMatch) {return { error: 'Failed to parse Signature-Input - missing components list' };}const componentsStr = componentsMatch[1];const components = componentsStr.split(/\\s+/).map(comp => comp.replace(/"/g, ''));// 解析参数const paramsStr = paramString.substring(componentsMatch[0].length);const params = {};const paramRegex = /;(\\w+)=([^;]+)/g;let paramMatch;while ((paramMatch = paramRegex.exec(paramsStr)) !== null) {const [, key, value] = paramMatch;if (value.startsWith('"') && value.endsWith('"')) {params[key] = value.slice(1, -1);} else if (/^\\d+$/.test(value)) {params[key] = parseInt(value, 10);} else {params[key] = value;}}// 重构参数字符串const signatureParams = `(${components.map(c => `"${c}"`).join(' ')})` +Object.entries(params).map(([key, value]) => {return typeof value === 'string' ? `;${key}="${value}"` : `;${key}=${value}`;}).join('');return { signatureId, components, params, signatureParams };} catch (error) {console.error(`[parseSignatureInput] ${error.stack}`);return { error: `Failed to parse Signature-Input: ${error.message}` }}}/*** 解析 Signature 请求头* @param {string} signature - Signature 请求头值* @param {string} signatureId - 签名 ID* @returns {{ error?: string; bytes: Uint8Array }} 解析结果*/function parseSignature(signature, signatureId) {try {const pattern = new RegExp(`${signatureId}=:([A-Za-z0-9+/=_-]+):`);const match = signature.match(pattern);if (!match) {return { error: `Failed to parse Signature - signature for ${signatureId} not found` };}let signatureB64 = match[1];// 转换 base64url 到标准 base64signatureB64 = signatureB64.replace(/-/g, '+').replace(/_/g, '/');while (signatureB64.length % 4 !== 0) {signatureB64 += '=';}// 解码为字节数组const binaryString = atob(signatureB64);const bytes = new Uint8Array(binaryString.length);for (let i = 0; i < binaryString.length; i++) {bytes[i] = binaryString.charCodeAt(i);}return { bytes };} catch (error) {console.error(`[parseSignature] ${error.stack}`);return { error: `Failed to parse Signature: ${error.message}` };}}/*** 验证签名参数* @param {Record<string, string|number>} params - 签名参数* @returns {{ error?: string }} 验证结果*/function validateSignatureParams(params) {console.log(`[validateSignatureParams] ${JSON.stringify(params)}`);try {if (!params.keyid) {return { error: 'Failed to validate signature parameters - missing keyid parameter' };}if (!params.alg || params.alg !== 'ed25519') {return { error: 'Failed to validate signature parameters - invalid or missing algorithm parameter' };}if (!params.tag || params.tag !== 'web-bot-auth') {return { error: 'Failed to validate signature parameters - invalid or missing tag parameter' };}if (!params.created || !params.expires) {return { error: 'Failed to validate signature parameters - missing created or expires parameter' };}const now = Math.floor(Date.now() / 1000);const clockSkew = 300; // 5 分钟时钟偏移容忍度if (params.created > now + clockSkew) {return { error: 'Failed to validate signature parameters - created time is in the future' };}if (params.expires < now - clockSkew) {return { error: 'Failed to validate signature parameters - signature has expired' };}const maxAge = 3600; // 1 小时最大有效期if (params.expires - params.created > maxAge) {return { error: 'Failed to validate signature parameters - signature validity period too long' };}return {};} catch (error) {console.error(`[validateSignatureParams] ${error.stack}`);return { error: `Failed to validate signature parameters - ${error.message}` };}}/*** 查找公钥* @param {Array} keys - 密钥目录中的公钥数组* @param {string} keyid - 公钥 ID* @returns {{ kid: string; kty: string; crv: string; x: string; }|undefined} 公钥*/function findPublicKey(keys, keyid) {for (const key of keys) {if (key.kid === keyid) {return key;}}return null;}/*** 构建签名基础字符串* @param {Request} request - 请求对象* @param {string[]} components - 组件列表* @param {string} signatureParams - 签名参数* @returns {string} 签名基础字符串*/function buildSignatureBase(request, components, signatureParams) {const lines = [];for (const component of components) {const value = extractComponent(request, component);lines.push(`"${component.toLowerCase()}": ${value}`);}lines.push(`"@signature-params": ${signatureParams}`);return lines.join('\\n');}/*** Ed25519 签名验证* @param {string} signatureBase - 签名基础字符串* @param {Uint8Array} signature - 签名字节数组* @param {{ kid: string; kty: string; crv: string; x: string; }} jwk - 公钥 JWK* @returns {Promise<[boolean, string]>} 验证结果,[是否验证通过, 错误信息]*/async function verifyEd25519(signatureBase, signature, jwk) {try {// 验证 JWK 格式if (!jwk || jwk.kty !== 'OKP' || jwk.crv !== 'Ed25519') {return [false, 'Failed to verify Ed25519 signature - invalid JWK format'];}// 验证签名长度if (!signature || signature.length !== 64) {return [false, `Failed to verify Ed25519 signature - invalid signature length (${signature?.length || 'null'}), expected 64`];}// 解码公钥const publicKeyBytes = base64UrlDecode(jwk.x);if (publicKeyBytes.length !== 32) {return [false, `Failed to verify Ed25519 signature - invalid public key length (${publicKeyBytes.length}), expected 32`];}// 编码消息const message = new TextEncoder().encode(signatureBase);const result = await _verifyEd25519(signature, message, publicKeyBytes);return [result];} catch (error) {console.error(`[verifyEd25519] ${error.stack}`);return [false, `Failed to verify Ed25519 signature - ${error.message}`];}}/*** Ed25519 校验核心逻辑* @param {Uint8Array} signature - 64 字节签名* @param {Uint8Array} message - 消息* @param {Uint8Array} publicKey - 32 字节公钥* @returns {Promise<boolean>} 验证结果*/async function _verifyEd25519(signature, message, publicKey) {if (signature.length !== 64) throw new Error('Signature must be 64 bytes');if (publicKey.length !== 32) throw new Error('Public key must be 32 bytes');try {const POINT_G = Point.BASE;// 解析公钥点 Aconst A = Point.fromBytes(publicKey);// 解析签名的 R 部分const R = Point.fromBytes(signature.slice(0, 32));// 解析签名的 S 部分const S = Ed25519.bytesToNumLE(signature.slice(32, 64));if (S >= Ed25519.N) throw new Error('S out of range');// 计算 k = H(R || A || M)const hashable = concatBytes(R.toBytes(), A.toBytes(), message);const hashed = await sha512(hashable);const k = Ed25519.mod(Ed25519.bytesToNumLE(hashed), Ed25519.N);// 验证等式: [8][S]B = [8]R + [8][k]A// 等价于: ([8]R + [8][k]A) - [8][S]B = 0const SB = POINT_G.multiply(S);const kA = A.multiply(k);const RkA = R.add(kA);// 计算 RkA - SB 并清除辅因子,检查是否为零点const diff = RkA.add(SB.negate()).clearCofactor();return diff.isZero();} catch (error) {console.error(`[Ed25519] ${error.stack}`);return false;}}// ==================== 辅助函数 ====================/*** 提取组件值* @param {Request} request - 请求对象* @param {string} component - 组件* @returns {string} 组件值*/function extractComponent(request, component) {if (component.startsWith('@')) {// 派生组件const url = new URL(request.url);switch (component) {case '@method':return request.method.toUpperCase();case '@authority':return url.host;case '@scheme':return url.protocol.slice(0, -1);case '@target-uri':return request.url;case '@request-target':return `${url.pathname}${url.search}`;case '@path':return url.pathname;case '@query':return url.search.slice(1);default:throw new Error(`Failed to extract component - component ${component} is not supported`);}} else {// HTTP 头部const value = request.headers.get(component.toLowerCase());return value || '';}}/*** Base64URL 解码* @param {string} str - Base64URL 编码的字符串* @returns {Uint8Array} 解码后的字节数组*/function base64UrlDecode(str) {// 转换 base64url 到标准 base64let base64 = str.replace(/-/g, '+').replace(/_/g, '/');const padding = base64.length % 4;if (padding) {base64 += '='.repeat(4 - padding);}// 解码const binaryString = atob(base64);const bytes = new Uint8Array(binaryString.length);for (let i = 0; i < binaryString.length; i++) {bytes[i] = binaryString.charCodeAt(i);}return bytes;}// SHA-512 哈希async function sha512(message) {const msgBuffer = message instanceof Uint8Array ? message : new Uint8Array(message);const hashBuffer = await crypto.subtle.digest('SHA-512', msgBuffer);return new Uint8Array(hashBuffer);}// 拼接字节数组function concatBytes(...arrays) {const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);const result = new Uint8Array(totalLength);let offset = 0;for (const arr of arrays) {result.set(arr, offset);offset += arr.length;}return result;}// ==================== 辅助类 ====================// Ed25519 工具类 - 封装所有曲线参数和数学函数class Ed25519 {// Ed25519 曲线参数(静态属性)static P = BigInt('0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffed');static N = BigInt('0x1000000000000000000000000000000014def9dea2f79cd65812631a5cf5d3ed');static Gx = BigInt('0x216936d3cd6e53fec0a4e231fdd6dc5c692cc7609525a7b2c9562d608f25d51a');static Gy = BigInt('0x6666666666666666666666666666666666666666666666666666666666666658');static D = BigInt('0x52036cee2b6ffe738cc740797779e89800700a4d4141d8ab75eb4dca135978a3');static RM1 = BigInt('0x2b8324804fc1df0b2b4d00993dfbd7a72f431806ad2fe478c4ee1b274a0ea0b0');static B256 = BigInt(2) ** BigInt(256);// 模运算static mod(a, b = Ed25519.P) {const r = a % b;return r >= 0n ? r : b + r;}// 模逆static invert(num, md = Ed25519.P) {if (num === 0n || md <= 0n) throw new Error('Invalid inverse');let a = Ed25519.mod(num, md), b = md, x = 0n, y = 1n, u = 1n, v = 0n;while (a !== 0n) {const q = b / a, r = b % a;const m = x - u * q, n = y - v * q;b = a, a = r, x = u, y = v, u = m, v = n;}if (b !== 1n) throw new Error('No inverse');return Ed25519.mod(x, md);}// 字节转大整数(小端序)static bytesToNumLE(bytes) {let result = 0n;for (let i = 0; i < bytes.length; i++) {result += BigInt(bytes[i]) << (8n * BigInt(i));}return result;}// 大整数转字节(小端序)static numTo32bLE(num) {const bytes = new Uint8Array(32);let n = num;for (let i = 0; i < 32; i++) {bytes[i] = Number(n & 0xFFn);n >>= 8n;}return bytes;}// pow2(x, k) = x^(2^k) mod Pstatic pow2(x, power) {let r = x;while (power-- > 0n) {r = (r * r) % Ed25519.P;}return r;}// 计算 (p+3)/8 次幂static pow_2_252_3(x) {const x2 = (x * x) % Ed25519.P;const b2 = (x2 * x) % Ed25519.P;const b4 = (Ed25519.pow2(b2, 2n) * b2) % Ed25519.P;const b5 = (Ed25519.pow2(b4, 1n) * x) % Ed25519.P;const b10 = (Ed25519.pow2(b5, 5n) * b5) % Ed25519.P;const b20 = (Ed25519.pow2(b10, 10n) * b10) % Ed25519.P;const b40 = (Ed25519.pow2(b20, 20n) * b20) % Ed25519.P;const b80 = (Ed25519.pow2(b40, 40n) * b40) % Ed25519.P;const b160 = (Ed25519.pow2(b80, 80n) * b80) % Ed25519.P;const b240 = (Ed25519.pow2(b160, 80n) * b80) % Ed25519.P;const b250 = (Ed25519.pow2(b240, 10n) * b10) % Ed25519.P;const pow_p_5_8 = (Ed25519.pow2(b250, 2n) * x) % Ed25519.P;return { pow_p_5_8, b2 };}// 平方根计算static uvRatio(u, v) {const v3 = Ed25519.mod(v * v * v);const v7 = Ed25519.mod(v3 * v3 * v);const pow = Ed25519.pow_2_252_3(u * v7).pow_p_5_8;let x = Ed25519.mod(u * v3 * pow);const vx2 = Ed25519.mod(v * x * x);const root1 = x;const root2 = Ed25519.mod(x * Ed25519.RM1);const useRoot1 = vx2 === u;const useRoot2 = vx2 === Ed25519.mod(-u);const noRoot = vx2 === Ed25519.mod(-u * Ed25519.RM1);if (useRoot1) x = root1;if (useRoot2 || noRoot) x = root2;if ((Ed25519.mod(x) & 1n) === 1n) x = Ed25519.mod(-x);return { isValid: useRoot1 || useRoot2, value: x };}}// 扩展坐标点类class Point {constructor(X, Y, Z, T) {this.X = X;this.Y = Y;this.Z = Z;this.T = T;}// 从字节解码static fromBytes(bytes) {if (bytes.length !== 32) throw new Error('Invalid point bytes length');const normed = new Uint8Array(bytes);const lastByte = bytes[31];normed[31] = lastByte & ~0x80;const y = Ed25519.bytesToNumLE(normed);if (y >= Ed25519.P) throw new Error('Y coordinate out of range');const y2 = Ed25519.mod(y * y);const u = Ed25519.mod(y2 - 1n);const v = Ed25519.mod(Ed25519.D * y2 + 1n);let { isValid, value: x } = Ed25519.uvRatio(u, v);if (!isValid) throw new Error('Invalid point: y is not a square root');const isXOdd = (x & 1n) === 1n;const isLastByteOdd = (lastByte & 0x80) !== 0;if (isLastByteOdd !== isXOdd) x = Ed25519.mod(-x);return new Point(x, y, 1n, Ed25519.mod(x * y));}// 点加法add(other) {const A = Ed25519.mod(this.X * other.X);const B = Ed25519.mod(this.Y * other.Y);const C = Ed25519.mod(this.T * Ed25519.D * other.T);const D2 = Ed25519.mod(this.Z * other.Z);const E = Ed25519.mod((this.X + this.Y) * (other.X + other.Y) - A - B);const F = Ed25519.mod(D2 - C);const G = Ed25519.mod(D2 + C);const H = Ed25519.mod(B + A);const X3 = Ed25519.mod(E * F);const Y3 = Ed25519.mod(G * H);const T3 = Ed25519.mod(E * H);const Z3 = Ed25519.mod(F * G);return new Point(X3, Y3, Z3, T3);}// 点倍乘double() {const A = Ed25519.mod(this.X * this.X);const B = Ed25519.mod(this.Y * this.Y);const C = Ed25519.mod(2n * Ed25519.mod(this.Z * this.Z));const D = Ed25519.mod(-A);const E = Ed25519.mod((this.X + this.Y) * (this.X + this.Y) - A - B);const G = D + B;const F = G - C;const H = D - B;const X3 = Ed25519.mod(E * F);const Y3 = Ed25519.mod(G * H);const T3 = Ed25519.mod(E * H);const Z3 = Ed25519.mod(F * G);return new Point(X3, Y3, Z3, T3);}// 标量乘法multiply(scalar) {const POINT_ZERO = Point.ZERO;let p = POINT_ZERO;let d = this;let n = scalar;while (n > 0n) {if (n & 1n) p = p.add(d);d = d.double();n >>= 1n;}return p;}// 取负negate() {return new Point(Ed25519.mod(-this.X), this.Y, this.Z, Ed25519.mod(-this.T));}// 判断是否为零点isZero() {return this.X === 0n && this.Y === this.Z;}// 清除辅因子clearCofactor() {return this.double().double().double();}// 转换为字节toBytes() {const iz = Ed25519.invert(this.Z);const x = Ed25519.mod(this.X * iz);const y = Ed25519.mod(this.Y * iz);const bytes = Ed25519.numTo32bLE(y);bytes[31] |= (x & 1n) ? 0x80 : 0;return bytes;}// 基点(惰性初始化)static get BASE() {if (!this._BASE) {this._BASE = new Point(Ed25519.Gx, Ed25519.Gy, 1n, Ed25519.mod(Ed25519.Gx * Ed25519.Gy));}return this._BASE;}// 零点(惰性初始化)static get ZERO() {if (!this._ZERO) {this._ZERO = new Point(0n, 1n, 1n, 0n);}return this._ZERO;}}
示例预览
1. 携带签名信息请求源站,且签名认证通过,响应源站内容:

2. 未携带签名信息请求源站,响应 401:

部署指南
架构准备
Web Bot Auth 认证需要具备以下三大核心组件:
边缘函数:部署在 EdgeOne 边缘节点,验证请求中携带的签名信息,进行 Agent 身份认证。
Agent 公钥目录服务:由 Agent 方独立托管的公钥目录服务。
Agent 客户端:需要验证身份的 Agent 客户端 。
Agent 生成密钥对
说明:
当前解决方案仅支持 Ed25519 密钥算法。
需要生成一对签名密钥,其中私钥用于签署 Agent 客户端的请求,公钥需要托管到特定的目录服务,以供边缘函数验证签名。
公钥需要转换为 JSON Web Key (JWK) 格式,转换后需要包含
kty、crv、x 三个必要字段,以及需要计算 JWK 指纹(Thumbprint)作为密钥标识符 kid。部署公钥目录服务
Agent 公钥目录服务必须满足以下接口规范:
1. 响应头部
Content-Type 的值必须为 application/http-message-signatures-directory+json。2. 必须包含响应头部
Signature-Input,其格式需为:sig1=("@method" "@target-uri" "@authority");alg="ed25519";keyid="<kid>";tag="web-bot-auth";created=<timestamp>;expires=<timestamp>
其中
kid 为公钥的标识符。3. 响应体格式如下:
{"keys": [{"kid": "公钥的标识符,可选","kty": "OKP","crv": "Ed25519","x": "公钥的 x 坐标(Base64URL 编码)"}]}
客户端请求签名
客户端需要正确生成签名并添加必要的请求头
Signature、 Signature-Input 以及 Signature-Agent。本实现支持以下派生组件,可根据安全需求选择:
组件 | 说明 | 是否支持 |
@method | HTTP 请求方法 | 支持 |
@target-uri | 完整请求 URI(RFC 9421 标准) | 支持 |
@authority | 请求主机 | 支持 |
@scheme | 协议(HTTP / HTTPS) | 支持 |
@request-target | 请求路径+查询参数 | 支持 |
@path | 请求路径 | 支持 |
@query | 查询参数 | 支持 |
@query-param | 单个查询参数 | 不支持 |
content-digest | 请求体摘要 | 不支持 |
相关参考