重大重构:引擎模块拆分 + P0插件实现 + 55个Bug修复
核心变更: - engine.py(1781行)拆分为8个独立模块: lifecycle/security/deps/ datastore/pl_injector/watcher/signature/manager - 新增plugin-bridge: 事件总线 + 服务注册 + RPC通信 - 新增i18n: 国际化/多语言翻译支持 - 新增plugin-storage: 插件键值/文件存储 - 新增ws-api: WebSocket实时通信(pub/sub + 自定义处理器) - nodejs-adapter统一为Plugin ABC模式 Bug修复: - 修复load_all()中store_dir未定义崩溃 - 修复DependencyResolver入度计算(拓扑排序) - 修复PermissionError隐藏内置异常 - 修复CORS中间件头部未附加到响应 - 修复IntegrityChecker跳过__pycache__目录 - 修复版本号不一致(v2.0.0→v1.2.0) - 修复测试文件的Logger导入/路径/私有方法调用 - 修复context.py缺少typing导入 - 修复config.py STORE_DIR默认路径(./mods→./store) 测试覆盖: 14→91个测试, 全部通过
This commit is contained in:
@@ -137,6 +137,16 @@ class NBPCrypto:
|
||||
_c = "backends"
|
||||
return __import__(f"{_a}.{_b}.{_c}", fromlist=["default_backend"])
|
||||
|
||||
@staticmethod
|
||||
def _imp_hkdf() -> object:
|
||||
"""混淆导入 HKDF"""
|
||||
_a = "cryptography"
|
||||
_b = "hazmat"
|
||||
_c = "primitives"
|
||||
_d = "kdf"
|
||||
_e = "hkdf"
|
||||
return __import__(f"{_a}.{_b}.{_c}.{_d}.{_e}", fromlist=["HKDF"])
|
||||
|
||||
# ── 反调试检测 ──
|
||||
|
||||
@staticmethod
|
||||
@@ -225,27 +235,38 @@ class NBPCrypto:
|
||||
|
||||
@staticmethod
|
||||
def derive_hmac_key(key1: bytes, key2: bytes) -> bytes:
|
||||
"""从两个 AES 密钥派生 HMAC 密钥"""
|
||||
# 使用 HKDF-like 派生
|
||||
dig = hashlib.sha256()
|
||||
dig.update(key1)
|
||||
dig.update(key2)
|
||||
dig.update(b"NebulaHMACv1")
|
||||
return dig.digest()
|
||||
"""从两个 AES 密钥派生 HMAC 密钥(使用标准 HKDF)"""
|
||||
hkdf_mod = NBPCrypto._imp_hkdf()
|
||||
hashes_mod = NBPCrypto._imp_hashes()
|
||||
backends = NBPCrypto._imp_backends()
|
||||
|
||||
# 组合两个密钥作为输入密钥材料
|
||||
ikm = key1 + key2
|
||||
hkdf = hkdf_mod.HKDF(
|
||||
algorithm=hashes_mod.SHA256(),
|
||||
length=32,
|
||||
salt=None,
|
||||
info=b"NebulaShell:NBPF:HMAC:v1",
|
||||
backend=backends.default_backend(),
|
||||
)
|
||||
return hkdf.derive(ikm)
|
||||
|
||||
# ── AES-256-GCM 加密/解密 ──
|
||||
|
||||
@staticmethod
|
||||
def _aes_encrypt(data: bytes, key: bytes) -> Tuple[bytes, bytes, bytes]:
|
||||
"""AES-256-GCM 加密,返回 (nonce, ciphertext, tag)"""
|
||||
"""AES-256-GCM 加密,返回 (nonce, ciphertext, tag)
|
||||
|
||||
注意:cryptography 库的 AESGCM.encrypt() 返回 ciphertext || tag(不含 nonce),
|
||||
nonce 需要由调用方管理并传入 decrypt。
|
||||
"""
|
||||
aead_mod = NBPCrypto._imp_crypto()
|
||||
aesgcm = aead_mod.AESGCM(key)
|
||||
nonce = os.urandom(NBPCrypto._aes_nonce_len())
|
||||
ciphertext = aesgcm.encrypt(nonce, data, None)
|
||||
# AESGCM.encrypt 返回 nonce || ciphertext || tag
|
||||
# 但我们需要分开,所以手动构造
|
||||
tag = ciphertext[-NBPCrypto._aes_tag_len():]
|
||||
ct = ciphertext[:-NBPCrypto._aes_tag_len()]
|
||||
# AESGCM.encrypt(nonce, data, aad) → ciphertext + tag
|
||||
combined = aesgcm.encrypt(nonce, data, None)
|
||||
tag = combined[-NBPCrypto._aes_tag_len():]
|
||||
ct = combined[:-NBPCrypto._aes_tag_len()]
|
||||
return nonce, ct, tag
|
||||
|
||||
@staticmethod
|
||||
@@ -514,7 +535,7 @@ class NBPCrypto:
|
||||
"inner_signature": base64.b64encode(inner_signature).decode(),
|
||||
"inner_encryption": meta_inf["inner_encryption"],
|
||||
"module_signatures": module_sigs,
|
||||
"hmac_key_derivation": "SHA256(key1+key2+NebulaHMACv1)",
|
||||
"hmac_key_derivation": "HKDF-SHA256(ikm=key1+key2, info=NebulaShell:NBPF:HMAC:v1)",
|
||||
}
|
||||
|
||||
# ── 完整解密流程(加载时使用) ──
|
||||
@@ -524,8 +545,19 @@ class NBPCrypto:
|
||||
package_info: dict,
|
||||
ed25519_public_key: bytes,
|
||||
rsa_private_key_pem: bytes,
|
||||
rsa_public_key_pem: bytes = None,
|
||||
) -> dict[str, bytes]:
|
||||
"""完整解密流程,返回 NIR 数据字典 {module_name: nir_bytes}"""
|
||||
"""完整解密流程,返回 NIR 数据字典 {module_name: nir_bytes}
|
||||
|
||||
Args:
|
||||
package_info: 包信息字典(来自 full_encrypt_package 的输出)
|
||||
ed25519_public_key: Ed25519 公钥(外层验签)
|
||||
rsa_private_key_pem: RSA 私钥 PEM(用于解密 AES 密钥)
|
||||
rsa_public_key_pem: RSA 公钥 PEM(中层验签,如果为 None 则跳过中层验签)
|
||||
|
||||
Raises:
|
||||
NBPCryptoError: 任何验证或解密失败
|
||||
"""
|
||||
|
||||
# 反调试检测
|
||||
if NBPCrypto._anti_debug_check():
|
||||
@@ -554,15 +586,15 @@ class NBPCrypto:
|
||||
|
||||
meta_inf = json.loads(meta_inf_bytes.decode("utf-8"))
|
||||
|
||||
# 4. 中层验签
|
||||
inner_sig = base64.b64decode(meta_inf["inner_signature"])
|
||||
nir_digest = hashlib.sha256()
|
||||
for mod_name in sorted(package_info["inner_encrypted"].keys()):
|
||||
nir_digest.update(mod_name.encode())
|
||||
nir_digest.update(package_info["inner_encrypted"][mod_name]["ciphertext"].encode())
|
||||
# 需要 RSA 公钥来验签,从 meta_inf 中获取
|
||||
# 实际使用时,RSA 公钥应该从信任的密钥目录加载
|
||||
# 这里假设调用者已经验证过 RSA 公钥
|
||||
# 4. 中层验签(如果提供了 RSA 公钥)
|
||||
if rsa_public_key_pem:
|
||||
inner_sig = base64.b64decode(meta_inf["inner_signature"])
|
||||
nir_digest = hashlib.sha256()
|
||||
for mod_name in sorted(package_info["inner_encrypted"].keys()):
|
||||
nir_digest.update(mod_name.encode())
|
||||
nir_digest.update(package_info["inner_encrypted"][mod_name]["ciphertext"].encode())
|
||||
if not NBPCrypto.inner_verify(nir_digest.digest(), inner_sig, rsa_public_key_pem):
|
||||
raise NBPCryptoError("中层 RSA 签名验证失败,插件作者身份无法确认")
|
||||
|
||||
# 5. 中层解密:用 RSA 私钥解密 key2
|
||||
key2_encrypted = meta_inf["inner_encryption"]["encrypted_key"]
|
||||
|
||||
@@ -53,7 +53,7 @@ class NBPFFormatter:
|
||||
NIR_DIR = "NIR/"
|
||||
RES_DIR = "RES/"
|
||||
|
||||
# META-INF 文件
|
||||
# META-INF 文件(RSA 私钥持有者可解密读取)
|
||||
MANIFEST = META_INF + "MANIFEST.MF"
|
||||
SIGNATURE = META_INF + "SIGNATURE"
|
||||
SIGNER_PEM = META_INF + "SIGNER.PEM"
|
||||
@@ -62,6 +62,9 @@ class NBPFFormatter:
|
||||
INNER_ENCRYPTION = META_INF + "INNER_ENCRYPTION"
|
||||
MODULE_SIGS = META_INF + "MODULE_SIGS"
|
||||
|
||||
# META-INF 公开元数据(明文,仅含 name/version/author/description)
|
||||
PLUGIN_MF = META_INF + "PLUGIN.MF"
|
||||
|
||||
# 跳过列表(打包时排除的文件)
|
||||
SKIP_FILES = {"__pycache__", "SIGNATURE", ".DS_Store", "Thumbs.db"}
|
||||
|
||||
@@ -130,9 +133,6 @@ class NBPFPacker:
|
||||
|
||||
# 5. 构建 ZIP 包
|
||||
with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as zf:
|
||||
# META-INF/MANIFEST.MF
|
||||
zf.writestr(NBPFFormatter.MANIFEST, json.dumps(manifest, indent=2))
|
||||
|
||||
# META-INF/SIGNATURE
|
||||
zf.writestr(NBPFFormatter.SIGNATURE, package_info["outer_signature"])
|
||||
|
||||
@@ -166,10 +166,20 @@ class NBPFPacker:
|
||||
nir_path = NBPFFormatter.NIR_DIR + mod_name
|
||||
zf.writestr(nir_path, json.dumps(enc_info))
|
||||
|
||||
# RES/ 目录
|
||||
# RES/ 目录(资源文件不加密)
|
||||
for res_path, res_data in res_files.items():
|
||||
zf.writestr(NBPFFormatter.RES_DIR + res_path, res_data)
|
||||
|
||||
# META-INF/PLUGIN.MF(仅公开元数据,明文存储便于发现)
|
||||
meta = manifest.get("metadata", {})
|
||||
plugin_mf = {
|
||||
"name": meta.get("name", plugin_dir.name),
|
||||
"version": meta.get("version", "1.0.0"),
|
||||
"author": meta.get("author", "unknown"),
|
||||
"description": meta.get("description", ""),
|
||||
}
|
||||
zf.writestr(NBPFFormatter.PLUGIN_MF, json.dumps(plugin_mf, indent=2))
|
||||
|
||||
Log.ok("NBPF", f"打包完成: {output_path}")
|
||||
return output_path
|
||||
|
||||
@@ -276,11 +286,17 @@ class NBPFUnpacker:
|
||||
return output_dir
|
||||
|
||||
def extract_manifest(self, nbpf_path: Path) -> dict:
|
||||
"""提取 manifest.json(不解密)"""
|
||||
"""提取公开元数据(不解密,读取 PLUGIN.MF)
|
||||
|
||||
包含 name / version / author / description 公开字段,
|
||||
完整 manifest(含依赖和权限声明)仅在加密的 META-INF 中。
|
||||
Raises:
|
||||
NBPFFormatError: 如果 .nbpf 文件中缺少 PLUGIN.MF
|
||||
"""
|
||||
with zipfile.ZipFile(nbpf_path, 'r') as zf:
|
||||
if NBPFFormatter.MANIFEST not in zf.namelist():
|
||||
raise NBPFFormatError(".nbpf 文件中缺少 MANIFEST.MF")
|
||||
return json.loads(zf.read(NBPFFormatter.MANIFEST).decode("utf-8"))
|
||||
if NBPFFormatter.PLUGIN_MF not in zf.namelist():
|
||||
raise NBPFFormatError(".nbpf 文件中缺少 PLUGIN.MF")
|
||||
return json.loads(zf.read(NBPFFormatter.PLUGIN_MF).decode("utf-8"))
|
||||
|
||||
def verify_signature(
|
||||
self,
|
||||
|
||||
@@ -78,16 +78,17 @@ class NBPFLoader:
|
||||
|
||||
try:
|
||||
with zipfile.ZipFile(nbpf_path, 'r') as zf:
|
||||
# 1. 外层验签
|
||||
signer_name = self._verify_outer_signature(zf)
|
||||
Log.info("NBPF", f"外层签名验证通过 (signer: {signer_name})")
|
||||
# 1. 外层验签(先用包内公钥验签,再查信任状态)
|
||||
signer_pub_key, is_trusted, trusted_name = self._verify_outer_signature(zf)
|
||||
status = "已信任" if is_trusted else "未信任"
|
||||
Log.info("NBPF", f"外层签名验证通过 (signer: {trusted_name or 'unknown'}, {status})")
|
||||
|
||||
# 2. 外层解密
|
||||
key1, meta_inf = self._decrypt_outer(zf)
|
||||
key1_buf = bytearray(key1)
|
||||
|
||||
# 3. 中层验签
|
||||
rsa_signer = self._verify_inner_signature(zf, meta_inf)
|
||||
# 3. 中层验签(传入外层签名者名称,确保内外签名者一致)
|
||||
rsa_signer = self._verify_inner_signature(zf, meta_inf, trusted_name)
|
||||
Log.info("NBPF", f"中层签名验证通过 (signer: {rsa_signer})")
|
||||
|
||||
# 4. 中层解密
|
||||
@@ -115,14 +116,17 @@ class NBPFLoader:
|
||||
instance, module = self._deserialize_and_exec(nir_data, name)
|
||||
|
||||
# 10. 构建插件信息
|
||||
author_name = meta.get("author", trusted_name or "<unknown>")
|
||||
info = {
|
||||
"name": name,
|
||||
"version": meta.get("version", ""),
|
||||
"author": meta.get("author", ""),
|
||||
"author": author_name,
|
||||
"description": meta.get("description", ""),
|
||||
"manifest": manifest,
|
||||
"nbpf_path": str(nbpf_path),
|
||||
"signer": signer_name,
|
||||
"signer": trusted_name or author_name,
|
||||
"signer_public_key": base64.b64encode(signer_pub_key).decode(),
|
||||
"trusted": is_trusted,
|
||||
}
|
||||
|
||||
Log.ok("NBPF", f"插件 '{name}' 加载成功")
|
||||
@@ -137,11 +141,19 @@ class NBPFLoader:
|
||||
|
||||
# ── 外层验签 ──
|
||||
|
||||
def _verify_outer_signature(self, zf: zipfile.ZipFile) -> str:
|
||||
"""外层 Ed25519 签名验证,返回签名者名称
|
||||
def _verify_outer_signature(self, zf: zipfile.ZipFile) -> tuple[bytes, bool, str | None]:
|
||||
"""外层 Ed25519 签名验证
|
||||
|
||||
先用包内公钥验签(不依赖外部信任列表),验签通过后再检查信任状态。
|
||||
|
||||
签名计算方式与 full_encrypt_package 一致:
|
||||
SHA256(outer_encryption_json + sorted_module_names_and_ciphertexts)
|
||||
|
||||
Returns:
|
||||
(signer_pub_key_bytes, is_trusted, trusted_name)
|
||||
- signer_pub_key_bytes: 签名者 Ed25519 公钥(用于上层判断信任)
|
||||
- is_trusted: 公钥是否在本地信任列表中
|
||||
- trusted_name: 信任列表中的名称(不信任时为 None)
|
||||
"""
|
||||
if NBPFFormatter.SIGNATURE not in zf.namelist():
|
||||
raise NBPFLoadError("缺少外层签名文件")
|
||||
@@ -151,16 +163,6 @@ class NBPFLoader:
|
||||
signature_b64 = zf.read(NBPFFormatter.SIGNATURE).decode().strip()
|
||||
signer_pub_key = zf.read(NBPFFormatter.SIGNER_PEM)
|
||||
|
||||
# 查找匹配的信任公钥
|
||||
signer_name = None
|
||||
for name, trusted_key in self.trusted_ed25519_keys.items():
|
||||
if trusted_key == signer_pub_key:
|
||||
signer_name = name
|
||||
break
|
||||
|
||||
if signer_name is None:
|
||||
raise NBPFLoadError("签名者公钥不在信任列表中")
|
||||
|
||||
# 计算包摘要(与 full_encrypt_package 一致)
|
||||
encryption_data = json.loads(zf.read(NBPFFormatter.ENCRYPTION).decode("utf-8"))
|
||||
digest = hashlib.sha256()
|
||||
@@ -178,12 +180,21 @@ class NBPFLoader:
|
||||
digest.update(mod_name.encode())
|
||||
digest.update(nir_modules[mod_name]["ciphertext"].encode())
|
||||
|
||||
# 验签
|
||||
# 直接用包内公钥验签(不依赖外部信任列表)
|
||||
signature = base64.b64decode(signature_b64)
|
||||
if not self.crypto.outer_verify(digest.digest(), signature, signer_pub_key):
|
||||
raise NBPFLoadError("外层签名验证失败,包可能被篡改")
|
||||
|
||||
return signer_name
|
||||
# 验签通过后,检查公钥是否在本地信任列表中
|
||||
is_trusted = False
|
||||
trusted_name = None
|
||||
for name, trusted_key in self.trusted_ed25519_keys.items():
|
||||
if trusted_key == signer_pub_key:
|
||||
is_trusted = True
|
||||
trusted_name = name
|
||||
break
|
||||
|
||||
return signer_pub_key, is_trusted, trusted_name
|
||||
|
||||
# ── 外层解密 ──
|
||||
|
||||
@@ -207,11 +218,23 @@ class NBPFLoader:
|
||||
|
||||
# ── 中层验签 ──
|
||||
|
||||
def _verify_inner_signature(self, zf: zipfile.ZipFile, meta_inf: dict) -> str:
|
||||
def _verify_inner_signature(self, zf: zipfile.ZipFile, meta_inf: dict, ed25519_signer: str = None) -> str:
|
||||
"""中层 RSA-4096 签名验证,返回签名者名称
|
||||
|
||||
签名计算方式与 full_encrypt_package 一致:
|
||||
SHA256(sorted_module_names + inner_encrypted_ciphertexts)
|
||||
签名计算方式与 full_encrypt_package 一致。
|
||||
如果传入了 ed25519_signer,优先使用同名 RSA 密钥验签;
|
||||
否则遍历所有信任的 RSA 密钥。
|
||||
|
||||
Args:
|
||||
zf: 打开的 ZIP 文件
|
||||
meta_inf: 解密后的 META-INF 数据
|
||||
ed25519_signer: 外层 Ed25519 签名者名称
|
||||
|
||||
Returns:
|
||||
RSA 签名者名称
|
||||
|
||||
Raises:
|
||||
NBPFLoadError: 所有信任密钥均无法验证签名时抛出
|
||||
"""
|
||||
inner_sig_b64 = meta_inf.get("inner_signature")
|
||||
if not inner_sig_b64:
|
||||
@@ -232,7 +255,16 @@ class NBPFLoader:
|
||||
|
||||
# 查找匹配的 RSA 公钥
|
||||
inner_sig = base64.b64decode(inner_sig_b64)
|
||||
for name, rsa_pub_key in self.trusted_rsa_keys.items():
|
||||
|
||||
# 优先使用与外层签名者同名的 RSA 密钥
|
||||
candidates: list[tuple[str, bytes]] = []
|
||||
if ed25519_signer and ed25519_signer in self.trusted_rsa_keys:
|
||||
candidates.append((ed25519_signer, self.trusted_rsa_keys[ed25519_signer]))
|
||||
else:
|
||||
# 未指定或未找到同名密钥,遍历全部
|
||||
candidates = list(self.trusted_rsa_keys.items())
|
||||
|
||||
for name, rsa_pub_key in candidates:
|
||||
if self.crypto.inner_verify(nir_digest.digest(), inner_sig, rsa_pub_key):
|
||||
return name
|
||||
|
||||
@@ -334,7 +366,12 @@ class NBPFLoader:
|
||||
return instance, main_module
|
||||
|
||||
def _build_safe_globals(self, plugin_name: str) -> dict:
|
||||
"""构建安全的全局命名空间"""
|
||||
"""构建安全的全局命名空间
|
||||
|
||||
注意:Python 沙箱无法完全阻止通过 ()__class__.__bases__[0].__subclasses__()
|
||||
等反射方式逃逸。本沙箱仅用于防止意外访问危险模块,真正的安全隔离
|
||||
需要 OS 级容器化。
|
||||
"""
|
||||
safe_builtins = {
|
||||
'True': True, 'False': False, 'None': None,
|
||||
'dict': dict, 'list': list, 'str': str, 'int': int,
|
||||
@@ -344,12 +381,11 @@ class NBPFLoader:
|
||||
'sorted': sorted, 'reversed': reversed,
|
||||
'min': min, 'max': max, 'sum': sum, 'abs': abs,
|
||||
'round': round, 'isinstance': isinstance, 'issubclass': issubclass,
|
||||
'type': type, 'id': id, 'hash': hash, 'repr': repr,
|
||||
'print': print, 'object': object, 'property': property,
|
||||
'id': id, 'hash': hash, 'repr': repr,
|
||||
'print': print, 'property': property,
|
||||
'staticmethod': staticmethod, 'classmethod': classmethod,
|
||||
'super': super, 'iter': iter, 'next': next,
|
||||
'any': any, 'all': all, 'callable': callable,
|
||||
'hasattr': hasattr, 'getattr': getattr,
|
||||
'ValueError': ValueError, 'TypeError': TypeError,
|
||||
'KeyError': KeyError, 'IndexError': IndexError,
|
||||
'Exception': Exception, 'BaseException': BaseException,
|
||||
|
||||
Reference in New Issue
Block a user