feat: MeshSafe 文件摆渡安全卫士 v1.0 初始提交

核心功能:
- 6 层检测管线: L0 结构分析 → L1 ClamAV → L2 文档深度检测 → L3 关键词匹配 → L4 YARA → L5 PE/二进制
- FastAPI + PostgreSQL + Redis + Docker Compose 架构
- 客户端 HMAC-SHA256 签名鉴权 + 限流 + 幂等性
- 控制台 UI (登录/Dashboard/诊断中心/策略管理/任务列表/测试中心/产品介绍页)
- 三层诊断定时器 (error_scan 30s / db_aggregate 30s / container_check 5min)

并发与稳定性修复:
- 诊断定时器 Redis 时间槽位锁 (多 worker 同一槽位只有一个执行)
- JSONL 存储 fcntl 跨进程排他锁 + 容错解码 + 原子重写
- 移除 SQLite fallback, 统一 PostgreSQL
- API_CLIENTS 脱敏为环境变量, .env 已 .gitignore
This commit is contained in:
2026-10-28 19:07:11 +08:00
commit 16f134e328
113 changed files with 24604 additions and 0 deletions

179
app/security/signing.py Normal file
View File

@@ -0,0 +1,179 @@
"""API 请求签名与响应签名。
请求签名规范(HMAC-SHA256)
---------------------------
客户端需在请求头中携带:
- ``X-MS-Client-Id`` 调用方标识
- ``X-MS-Timestamp`` Unix 秒级时间戳
- ``X-MS-Nonce`` 随机串,32 位十六进制,服务端保证一次性
- ``X-MS-Body-SHA256`` 请求体字节的 SHA256(无请求体时为空串的 SHA256)
- ``X-MS-Signature`` 签名值
待签名串由以下部分按固定顺序拼接,换行符为 ``\\n``::
METHOD
PATH
CANONICAL_QUERY
BODY_SHA256
TIMESTAMP
NONCE
CLIENT_ID
签名为 ``HMAC-SHA256(请求签名密钥, 待签名串)`` 的小写十六进制。
响应签名
--------
服务端在响应头中返回:
- ``X-MS-Response-Timestamp``
- ``X-MS-Response-Signature``: ``HMAC-SHA256(响应签名密钥, 响应体SHA256 + 时间戳)``
客户端用相同密钥派生规则即可独立验签,从而防止中间人篡改结论。
"""
from __future__ import annotations
import hashlib
import hmac
import json
from typing import Any, Dict, Mapping, Optional
EMPTY_BODY_SHA256 = hashlib.sha256(b"").hexdigest()
def canonical_query(params: Optional[Mapping[str, Any]]) -> str:
"""将查询参数规范化为 ``k=v&k2=v2`` 形式(按 key 升序)。"""
if not params:
return ""
items = []
for key in sorted(params.keys()):
value = params[key]
if value is None:
continue
if isinstance(value, (list, tuple)):
for item in value:
items.append(f"{key}={item}")
else:
items.append(f"{key}={value}")
return "&".join(items)
def body_sha256(body: bytes) -> str:
return hashlib.sha256(body).hexdigest()
def build_signing_string(
method: str,
path: str,
query: str,
body_hash: str,
timestamp: str,
nonce: str,
client_id: str,
) -> str:
return "\n".join(
[method.upper(), path, query, body_hash, str(timestamp), nonce, client_id]
)
def sign_request(
secret: str,
client_id: str,
method: str,
path: str,
query: Optional[Mapping[str, Any]] = None,
body: bytes = b"",
timestamp: Optional[int] = None,
nonce: Optional[str] = None,
) -> Dict[str, str]:
"""生成完整的请求签名头集合。"""
import os
import time
ts = str(timestamp if timestamp is not None else int(time.time()))
nc = nonce or os.urandom(16).hex()
bh = body_sha256(body)
qs = canonical_query(query)
payload = build_signing_string(method, path, qs, bh, ts, nc, client_id)
key = _signing_key(secret, client_id)
signature = hmac.new(key, payload.encode("utf-8"), hashlib.sha256).hexdigest()
return {
"X-MS-Client-Id": client_id,
"X-MS-Timestamp": ts,
"X-MS-Nonce": nc,
"X-MS-Body-SHA256": bh,
"X-MS-Signature": signature,
}
def verify_request_signature(
secret: str,
client_id: str,
method: str,
path: str,
query: Optional[Mapping[str, Any]],
body: bytes,
timestamp: str,
nonce: str,
provided_body_hash: str,
provided_signature: str,
*,
skip_body_hash_check: bool = False,
) -> bool:
"""服务端侧验签。
skip_body_hash_check=True 时(multipart/form-data 场景,服务端拿不到原始 body),
直接信任客户端提供的 provided_body_hash,只校验 HMAC 签名部分。
"""
if skip_body_hash_check:
expected_body_hash = provided_body_hash or ""
else:
expected_body_hash = body_sha256(body)
if not hmac.compare_digest(expected_body_hash, provided_body_hash or ""):
return False
payload = build_signing_string(
method, path, canonical_query(query), expected_body_hash, timestamp, nonce, client_id
)
key = _signing_key(secret, client_id)
expected = hmac.new(key, payload.encode("utf-8"), hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, provided_signature or "")
def sign_response(secret: str, client_id: str, body_bytes: bytes, timestamp: Optional[int] = None) -> Dict[str, str]:
"""为响应体生成签名头。"""
import time
ts = str(timestamp if timestamp is not None else int(time.time()))
key = _response_key(secret, client_id)
payload = body_sha256(body_bytes) + ts
signature = hmac.new(key, payload.encode("utf-8"), hashlib.sha256).hexdigest()
return {"X-MS-Response-Timestamp": ts, "X-MS-Response-Signature": signature}
def verify_response_signature(
secret: str, client_id: str, body_bytes: bytes, timestamp: str, signature: str
) -> bool:
key = _response_key(secret, client_id)
payload = body_sha256(body_bytes) + timestamp
expected = hmac.new(key, payload.encode("utf-8"), hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature or "")
def canonical_json_bytes(payload: Any) -> bytes:
"""将响应对象序列化为稳定的 JSON 字节串(供签名使用)。"""
return json.dumps(
payload, ensure_ascii=False, sort_keys=True, separators=(",", ":")
).encode("utf-8")
def _signing_key(secret: str, client_id: str) -> bytes:
from app.security.crypto import derive_client_signing_key
return derive_client_signing_key(secret, client_id)
def _response_key(secret: str, client_id: str) -> bytes:
from app.security.crypto import derive_client_response_key
return derive_client_response_key(secret, client_id)