255 lines
7.3 KiB
JavaScript
255 lines
7.3 KiB
JavaScript
// scripts/sfp-core.js
|
||
// 系统指纹安全协议 · SFP v1.0 · 核心模块
|
||
//
|
||
// 功能:
|
||
// ① generateSFP(agent_id, content) — 生成系统指纹
|
||
// ② verifySFP(content_with_signature) — 验证系统指纹
|
||
// ③ loadConfig() — 加载 SFP 配置
|
||
//
|
||
// 指纹格式: ⌜SFP::{agent_id}::{persona_chain}::{timestamp}::{content_hash}::{nonce}⌝
|
||
|
||
'use strict';
|
||
|
||
const crypto = require('crypto');
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
const ROOT = path.resolve(__dirname, '..');
|
||
const SFP_CONFIG_PATH = path.join(ROOT, 'data/security/sfp-config.json');
|
||
const SFP_NONCE_PATH = path.join(ROOT, 'data/security/sfp-nonce-registry.json');
|
||
const SFP_ALERT_PATH = path.join(ROOT, 'data/security/sfp-alert-log.json');
|
||
|
||
const BEIJING_OFFSET_MS = 8 * 3600 * 1000;
|
||
const MAX_NONCE_REGISTRY_SIZE = 1000;
|
||
const MAX_ALERT_LOG_SIZE = 500;
|
||
|
||
// ━━━ SFP 指纹正则 ━━━
|
||
const SFP_REGEX = /⌜SFP::([^:]+)::([^:]+)::([0-9T+:.-]+)::([a-f0-9]{12})::([a-zA-Z0-9]{6})⌝/;
|
||
|
||
// ━━━ 安全读取 JSON ━━━
|
||
function readJSON(filePath) {
|
||
try {
|
||
if (!fs.existsSync(filePath)) return null;
|
||
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||
} catch (e) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// ━━━ 安全写入 JSON ━━━
|
||
function writeJSON(filePath, data) {
|
||
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
|
||
}
|
||
|
||
// ━━━ 加载 SFP 配置 ━━━
|
||
function loadConfig() {
|
||
const config = readJSON(SFP_CONFIG_PATH);
|
||
if (!config) {
|
||
throw new Error('SFP配置文件缺失: ' + SFP_CONFIG_PATH);
|
||
}
|
||
return config;
|
||
}
|
||
|
||
// ━━━ 查找受信Agent ━━━
|
||
function findTrustedAgent(config, agentId) {
|
||
if (!config || !config.trusted_agents) return null;
|
||
return config.trusted_agents.find(a => a.agent_id === agentId) || null;
|
||
}
|
||
|
||
// ━━━ 计算内容哈希(SHA-256前12位) ━━━
|
||
function computeContentHash(content) {
|
||
const hash = crypto.createHash('sha256').update(content, 'utf8').digest('hex');
|
||
return hash.slice(0, 12);
|
||
}
|
||
|
||
// ━━━ 生成6位随机nonce ━━━
|
||
function generateNonce() {
|
||
// Use hex encoding of random bytes for unbiased distribution
|
||
return crypto.randomBytes(3).toString('hex');
|
||
}
|
||
|
||
// ━━━ 获取北京时间ISO字符串 ━━━
|
||
function getBeijingTimestamp() {
|
||
const now = new Date();
|
||
const bjTime = new Date(now.getTime() + BEIJING_OFFSET_MS);
|
||
return bjTime.toISOString().replace('Z', '+08:00').replace(/\.\d{3}/, '');
|
||
}
|
||
|
||
// ━━━ 生成系统指纹 ━━━
|
||
function generateSFP(agentId, content) {
|
||
const config = loadConfig();
|
||
const agent = findTrustedAgent(config, agentId);
|
||
|
||
if (!agent) {
|
||
return {
|
||
success: false,
|
||
error: 'AGENT_NOT_FOUND',
|
||
message: `Agent ${agentId} 不在受信Agent列表中`
|
||
};
|
||
}
|
||
|
||
const timestamp = getBeijingTimestamp();
|
||
const contentHash = computeContentHash(content);
|
||
const nonce = generateNonce();
|
||
|
||
const fingerprint = `⌜SFP::${agent.agent_id}::${agent.persona_chain}::${timestamp}::${contentHash}::${nonce}⌝`;
|
||
|
||
// 记录 nonce 到已使用列表
|
||
try {
|
||
const nonceRegistry = readJSON(SFP_NONCE_PATH) || { used_nonces: [] };
|
||
nonceRegistry.used_nonces.push({
|
||
nonce,
|
||
agent_id: agentId,
|
||
timestamp,
|
||
content_hash: contentHash
|
||
});
|
||
// 保留最近 1000 条 nonce 记录
|
||
if (nonceRegistry.used_nonces.length > MAX_NONCE_REGISTRY_SIZE) {
|
||
nonceRegistry.used_nonces = nonceRegistry.used_nonces.slice(-MAX_NONCE_REGISTRY_SIZE);
|
||
}
|
||
writeJSON(SFP_NONCE_PATH, nonceRegistry);
|
||
} catch (e) {
|
||
// nonce 记录失败不阻断指纹生成
|
||
}
|
||
|
||
return {
|
||
success: true,
|
||
fingerprint,
|
||
signed_content: `${content}\n${fingerprint}`,
|
||
meta: {
|
||
agent_id: agent.agent_id,
|
||
persona_chain: agent.persona_chain,
|
||
timestamp,
|
||
content_hash: contentHash,
|
||
nonce
|
||
}
|
||
};
|
||
}
|
||
|
||
// ━━━ 验证系统指纹 ━━━
|
||
function verifySFP(contentWithSignature) {
|
||
const config = loadConfig();
|
||
|
||
// Step 1: 检查是否有指纹块
|
||
const match = contentWithSignature.match(SFP_REGEX);
|
||
if (!match) {
|
||
return {
|
||
valid: false,
|
||
error: 'NO_FINGERPRINT',
|
||
level: '⚠️',
|
||
message: '无指纹·不可信',
|
||
log: `[SFP-WARN] 无指纹内容发现`
|
||
};
|
||
}
|
||
|
||
const [, agentId, personaChain, timestamp, contentHash, nonce] = match;
|
||
|
||
// Step 2: 验证 agent_id
|
||
const agent = findTrustedAgent(config, agentId);
|
||
if (!agent) {
|
||
return {
|
||
valid: false,
|
||
error: 'INVALID_AGENT',
|
||
level: '❌',
|
||
message: `无效Agent ID: ${agentId}`,
|
||
log: `[SFP-ALERT] 伪造Agent ID: ${agentId}`
|
||
};
|
||
}
|
||
|
||
// Step 3: 验证亲子链
|
||
if (agent.persona_chain !== personaChain) {
|
||
return {
|
||
valid: false,
|
||
error: 'CHAIN_MISMATCH',
|
||
level: '❌',
|
||
message: `亲子链伪造: 期望 ${agent.persona_chain},实际 ${personaChain}`,
|
||
log: `[SFP-CRITICAL] 亲子链伪造企图 · agent=${agentId}`
|
||
};
|
||
}
|
||
|
||
// Step 4: 验证内容哈希
|
||
// 从内容中去除指纹行,重新计算哈希
|
||
const fingerprintLine = match[0];
|
||
const originalContent = contentWithSignature
|
||
.replace(fingerprintLine, '')
|
||
.replace(/\n$/, '')
|
||
.trim();
|
||
const recomputedHash = computeContentHash(originalContent);
|
||
|
||
if (recomputedHash !== contentHash) {
|
||
return {
|
||
valid: false,
|
||
error: 'CONTENT_TAMPERED',
|
||
level: '❌',
|
||
message: `内容被篡改: hash不匹配 (期望=${contentHash}, 实际=${recomputedHash})`,
|
||
log: `[SFP-CRITICAL] 内容篡改检测 · hash不匹配`
|
||
};
|
||
}
|
||
|
||
// Step 5: 检查 nonce 重放
|
||
const nonceRegistry = readJSON(SFP_NONCE_PATH) || { used_nonces: [] };
|
||
const nonceUsed = nonceRegistry.used_nonces.some(
|
||
n => n.nonce === nonce && n.agent_id !== agentId
|
||
);
|
||
// 注意:同一个 agent 的相同 nonce 是自己生成的,允许验证
|
||
// 只有不同 agent 使用相同 nonce 才是重放攻击
|
||
const duplicateNonce = nonceRegistry.used_nonces.filter(n => n.nonce === nonce);
|
||
if (duplicateNonce.length > 1) {
|
||
// 检查是否有不同 agent 使用了相同 nonce
|
||
const uniqueAgents = new Set(duplicateNonce.map(n => n.agent_id));
|
||
if (uniqueAgents.size > 1) {
|
||
return {
|
||
valid: false,
|
||
error: 'REPLAY_ATTACK',
|
||
level: '❌',
|
||
message: `重放攻击: nonce ${nonce} 被多个Agent使用`,
|
||
log: `[SFP-CRITICAL] 重放攻击 · nonce重复 · nonce=${nonce}`
|
||
};
|
||
}
|
||
}
|
||
|
||
return {
|
||
valid: true,
|
||
level: '✅',
|
||
message: '指纹验证通过',
|
||
meta: {
|
||
agent_id: agentId,
|
||
persona_chain: personaChain,
|
||
timestamp,
|
||
content_hash: contentHash,
|
||
nonce,
|
||
agent_name: agent.name,
|
||
agent_side: agent.side
|
||
}
|
||
};
|
||
}
|
||
|
||
// ━━━ 记录安全警报 ━━━
|
||
function logAlert(alertEntry) {
|
||
try {
|
||
const alertLog = readJSON(SFP_ALERT_PATH) || { alerts: [] };
|
||
alertLog.alerts.push({
|
||
...alertEntry,
|
||
logged_at: getBeijingTimestamp()
|
||
});
|
||
// 保留最近 500 条
|
||
if (alertLog.alerts.length > MAX_ALERT_LOG_SIZE) {
|
||
alertLog.alerts = alertLog.alerts.slice(-MAX_ALERT_LOG_SIZE);
|
||
}
|
||
writeJSON(SFP_ALERT_PATH, alertLog);
|
||
} catch (e) {
|
||
console.error('[SFP] 写入警报日志失败:', e.message);
|
||
}
|
||
}
|
||
|
||
module.exports = {
|
||
generateSFP,
|
||
verifySFP,
|
||
loadConfig,
|
||
findTrustedAgent,
|
||
computeContentHash,
|
||
generateNonce,
|
||
logAlert,
|
||
SFP_REGEX
|
||
};
|