核心功能: - 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
180 lines
5.4 KiB
Python
180 lines
5.4 KiB
Python
"""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)
|