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:
179
app/security/signing.py
Normal file
179
app/security/signing.py
Normal 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)
|
||||
Reference in New Issue
Block a user