tencent cloud

边缘安全加速平台 EO

动态与公告
产品动态
安全公告
产品公告
产品简介
产品概述
产品优势
应用场景
EdgeOne 与 CDN 等产品功能对比
使用限制
购买指南
试用套餐体验权益说明
免费版套餐使用说明
计费概述
计费项目
购买指引
续费指引
欠费与退款说明
套餐选型对比
关于“干净流量”计费说明
DDoS 防护容量说明
快速入门
选择业务场景
快速接入网站安全加速
通过 Pages 快速部署网站
域名服务与源站配置
域名服务
HTTPS 证书
源站配置
站点加速
概述
访问控制
智能加速
缓存配置
文件优化
网络优化
URL 重写
修改头部
修改应答内容
规则引擎
图片与视频处理
单连接下载限速
DDoS 与 Web 防护
概述
DDoS 防护
Web 防护
Bot 管理
API 资产识别(Beta)
边缘函数
概述
快速指引
操作指引
Runtime APIs
示例函数
实践教程
Pages
四层代理
概述
新建四层代理实例
修改四层代理实例配置
停用/删除四层代理实例
批量配置转发规则
获取客户端真实IP
数据分析与日志服务
日志服务
数据分析
告警服务
站点与计费管理
计费管理
站点管理
版本管理
通用策略
通用参考
配置语法
请求与响应行为
国家/地区及对应代码枚举
Terraform
Terraform 简介
安装和配置 Terraform
实践教程
自动预热/清除缓存
防盗刷/盗链实践
HTTPS 相关实践
加速优化
流量调度
数据分析与告警
第三方日志平台集成实践
对象存储类源站(例如:COS)配置实践
跨域响应配置
API 文档
History
Introduction
API Category
Making API Requests
Site APIs
Acceleration Domain Management APIs
Site Acceleration Configuration APIs
Edge Function APIs
Alias Domain APIs
Security Configuration APIs
Layer 4 Application Proxy APIs
Content Management APIs
Data Analysis APIs
Log Service APIs
Billing APIs
Certificate APIs
Origin Protection APIs
Load Balancing APIs
Diagnostic Tool APIs
Custom Response Page APIs
API Security APIs
DNS Record APIs
Content Identifier APIs
Legacy APIs
Ownership APIs
Image and Video Processing APIs
Multi-Channel Security Gateway APIs
Version Management APIs
Data Types
Error Codes
常见问题
产品特性相关问题
DNS 记录相关问题
域名配置相关问题
站点加速相关问题
数据与日志相关问题
安全防护相关问题
源站配置相关问题
排障指南
异常状态码参考
EdgeOne 4XX/5XX 状态码排障指南
520/524状态码排障指南
521/522 状态码排障指南
工具指南
相关协议
Service Level Agreement
源站防护启用特别约定
TEO 政策
隐私协议
数据处理和安全协议
联系我们
词汇表

Web Bot Auth

PDF
聚焦模式
字号
最后更新时间: 2026-01-06 18:10:06
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}`;
}
// 获取密钥目录 URL
const 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 {
// 缓存验证通过,返回目录和已验证的 keyid
const 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-Type
const 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-Control
let 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 到标准 base64
signatureB64 = 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;
// 解析公钥点 A
const 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 = 0
const 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 到标准 base64
let 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 P
static 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) 格式,转换后需要包含 ktycrvx 三个必要字段,以及需要计算 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 编码)"
}
]
}

客户端请求签名

客户端需要正确生成签名并添加必要的请求头 SignatureSignature-Input 以及 Signature-Agent
本实现支持以下派生组件,可根据安全需求选择:
组件
说明
是否支持
@method
HTTP 请求方法
支持
@target-uri
完整请求 URI(RFC 9421 标准)
支持
@authority
请求主机
支持
@scheme
协议(HTTP / HTTPS)
支持
@request-target
请求路径+查询参数
支持
@path
请求路径
支持
@query
查询参数
支持
@query-param
单个查询参数
不支持
content-digest
请求体摘要
不支持

相关参考



帮助和支持

本页内容是否解决了您的问题?

填写满意度调查问卷,共创更好文档体验。

文档反馈