Files
NebulaShell/tests/test_nbpf.py
Falck 3a096f59a9 重构:核心迁移至 oss/core + NBPF 多重签名加密 + NIR 编译器 + README 全面升级
- 核心功能从 store/ 迁移至 oss/core/ 框架层
- 实现 NBPF 包格式:多重签名(Ed25519+RSA-PSS+HMAC)+ 多重加密(AES-256-GCM)
- 实现 NIR 编译器:基于 compile()+marshal 的跨平台中间表示
- 新增 nebula nbpf CLI 命令组(pack/unpack/verify/sign/keygen)
- 新增 19 个 NBPF 测试用例,覆盖全链路
- 彻底重写 README,大型项目标准框架风格,所有图表使用 SVG
- 更新 LICENSE 版权声明
- 清理旧版 store 插件目录(已迁移至 oss/core)
2026-05-05 07:29:43 +08:00

482 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""NBPF 模块测试"""
import sys
import os
import json
import tempfile
import shutil
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
from oss.core.nbpf import (
NBPCrypto, NBPCryptoError,
NIRCompiler, NIRCompileError,
NBPFPacker, NBPFUnpacker, NBPFFormatter,
NBPFLoader, NBPFLoadError,
)
# ═══════════════════════════════════════════════════════════════
# 辅助函数
# ═══════════════════════════════════════════════════════════════
def _create_test_plugin(tmp_dir: Path) -> Path:
"""创建测试插件目录"""
plugin_dir = tmp_dir / "test-plugin"
plugin_dir.mkdir(parents=True)
# manifest.json
manifest = {
"metadata": {
"name": "test-plugin",
"version": "1.0.0",
"author": "test",
"description": "测试插件",
},
"dependencies": [],
}
(plugin_dir / "manifest.json").write_text(json.dumps(manifest, indent=2))
# main.py
main_py = '''"""测试插件"""
class New:
def __init__(self):
self.name = "test-plugin"
self.started = False
def start(self):
self.started = True
return True
def stop(self):
self.started = False
return True
def greet(self, name: str) -> str:
return f"Hello, {name}!"
'''
(plugin_dir / "main.py").write_text(main_py)
return plugin_dir
def _generate_keys(tmp_dir: Path) -> dict:
"""生成测试密钥,返回路径字典"""
keys = {}
# Ed25519
ed25519_private, ed25519_public = NBPCrypto.generate_ed25519_keypair()
keys["ed25519_private"] = ed25519_private
keys["ed25519_public"] = ed25519_public
# RSA
rsa_private, rsa_public = NBPCrypto.generate_rsa_keypair(key_size=2048) # 测试用 2048
keys["rsa_private"] = rsa_private
keys["rsa_public"] = rsa_public
return keys
# ═══════════════════════════════════════════════════════════════
# 测试NBPCrypto
# ═══════════════════════════════════════════════════════════════
def test_crypto_aes_encrypt_decrypt():
"""测试 AES-256-GCM 加密解密"""
data = b"Hello, NebulaShell!"
key = NBPCrypto.generate_aes_key()
assert len(key) == 32
nonce, ct, tag = NBPCrypto._aes_encrypt(data, key)
assert len(nonce) == 12
assert len(tag) == 16
assert isinstance(ct, bytes)
dec = NBPCrypto._aes_decrypt(ct, key, nonce, tag)
assert dec == data
def test_crypto_outer_encrypt_decrypt():
"""测试外层加密解密"""
data = b"Secret plugin data"
key = NBPCrypto.generate_aes_key()
enc = NBPCrypto.outer_encrypt(data, key)
assert isinstance(enc, dict)
dec = NBPCrypto.outer_decrypt(enc, key)
assert dec == data
def test_crypto_inner_encrypt_decrypt():
"""测试中层加密解密"""
data = b"NIR compiled data"
key = NBPCrypto.generate_aes_key()
enc = NBPCrypto.inner_encrypt(data, key)
assert isinstance(enc, dict)
dec = NBPCrypto.inner_decrypt(enc, key)
assert dec == data
def test_crypto_ed25519_sign_verify():
"""测试 Ed25519 签名验证"""
data = b"Package content hash"
private, public = NBPCrypto.generate_ed25519_keypair()
signature = NBPCrypto.outer_sign(data, private)
assert isinstance(signature, bytes)
assert NBPCrypto.outer_verify(data, signature, public) is True
assert NBPCrypto.outer_verify(b"tampered", signature, public) is False
def test_crypto_rsa_sign_verify():
"""测试 RSA-PSS 签名验证"""
data = b"NIR content hash"
private, public = NBPCrypto.generate_rsa_keypair(key_size=2048)
signature = NBPCrypto.inner_sign(data, private)
assert isinstance(signature, bytes)
assert NBPCrypto.inner_verify(data, signature, public) is True
assert NBPCrypto.inner_verify(b"tampered", signature, public) is False
def test_crypto_hmac_sign_verify():
"""测试 HMAC-SHA256 签名验证"""
data = b"Module content"
key1 = NBPCrypto.generate_aes_key()
key2 = NBPCrypto.generate_aes_key()
hmac_key = NBPCrypto.derive_hmac_key(key1, key2)
signature = NBPCrypto.module_sign(data, hmac_key)
assert isinstance(signature, str)
assert NBPCrypto.module_verify(data, signature, hmac_key) is True
assert NBPCrypto.module_verify(b"tampered", signature, hmac_key) is False
def test_crypto_rsa_oaep_key_encapsulation():
"""测试 RSA-OAEP 密钥封装"""
aes_key = NBPCrypto.generate_aes_key()
rsa_private, rsa_public = NBPCrypto.generate_rsa_keypair(key_size=2048)
encrypted = NBPCrypto.encrypt_key(aes_key, rsa_public)
assert isinstance(encrypted, str)
decrypted = NBPCrypto.decrypt_key(encrypted, rsa_private)
assert decrypted == aes_key
def test_crypto_anti_debug():
"""测试反调试检测(不触发,仅验证函数存在)"""
assert hasattr(NBPCrypto, "_anti_debug_check")
assert callable(NBPCrypto._anti_debug_check)
def test_crypto_secure_wipe():
"""测试内存擦除"""
buf = bytearray(b"secret_key_1234567890")
NBPCrypto._secure_wipe(buf)
assert all(b == 0 for b in buf)
# ═══════════════════════════════════════════════════════════════
# 测试NIRCompiler
# ═══════════════════════════════════════════════════════════════
def test_compiler_compile_source():
"""测试编译源码为 NIR"""
compiler = NIRCompiler()
source = "x = 1\ny = x + 2\nresult = y * 3\n"
nir_data = compiler.compile_source(source, "test.py")
assert isinstance(nir_data, bytes)
# 反序列化
code = NIRCompiler.deserialize_nir(nir_data)
assert code is not None
# 执行
ns = {}
exec(code, ns)
assert ns["result"] == 9
def test_compiler_create_function():
"""测试从 code object 创建函数"""
compiler = NIRCompiler()
source = "def add(a, b): return a + b"
nir_data = compiler.compile_source(source, "test.py")
code = NIRCompiler.deserialize_nir(nir_data)
ns = {}
exec(code, ns)
assert ns["add"](2, 3) == 5
def test_compiler_forbidden_modules():
"""测试禁止导入危险模块"""
compiler = NIRCompiler()
dangerous_sources = [
"import os",
"from os import path",
"import subprocess",
"import sys",
"import socket",
"import ctypes",
]
for source in dangerous_sources:
try:
compiler.compile_source(source, "test.py")
assert False, f"应该拒绝: {source}"
except NIRCompileError:
pass # 预期行为
def test_compiler_compile_plugin():
"""测试编译整个插件目录"""
compiler = NIRCompiler()
with tempfile.TemporaryDirectory() as tmp:
plugin_dir = _create_test_plugin(Path(tmp))
nir_data = compiler.compile_plugin(plugin_dir)
assert "main" in nir_data
assert isinstance(nir_data["main"], bytes)
# ═══════════════════════════════════════════════════════════════
# 测试NBPFPacker + NBPFUnpacker
# ═══════════════════════════════════════════════════════════════
def _create_packer():
"""创建 NBPFPacker 实例"""
return NBPFPacker(crypto=NBPCrypto(), compiler=NIRCompiler())
def test_pack_unpack():
"""测试打包和解包完整流程"""
packer = _create_packer()
unpacker = NBPFUnpacker()
with tempfile.TemporaryDirectory() as tmp:
tmp_path = Path(tmp)
plugin_dir = _create_test_plugin(tmp_path)
keys = _generate_keys(tmp_path)
# 打包
nbpf_path = tmp_path / "test-plugin.nbpf"
result = packer.pack(
plugin_dir=plugin_dir,
output_path=nbpf_path,
ed25519_private_key=keys["ed25519_private"],
rsa_private_key_pem=keys["rsa_private"],
rsa_public_key_pem=keys["rsa_public"],
signer_name="test",
)
assert result.exists()
assert result.suffix == ".nbpf"
# 解包
unpack_dir = tmp_path / "unpacked"
result_dir = unpacker.unpack(nbpf_path, unpack_dir)
assert result_dir.exists()
# 解包后 manifest 在 META-INF/MANIFEST.MF
assert (result_dir / "META-INF" / "MANIFEST.MF").exists()
def test_extract_manifest():
"""测试提取 manifest"""
packer = _create_packer()
unpacker = NBPFUnpacker()
with tempfile.TemporaryDirectory() as tmp:
tmp_path = Path(tmp)
plugin_dir = _create_test_plugin(tmp_path)
keys = _generate_keys(tmp_path)
nbpf_path = tmp_path / "test-plugin.nbpf"
packer.pack(
plugin_dir=plugin_dir,
output_path=nbpf_path,
ed25519_private_key=keys["ed25519_private"],
rsa_private_key_pem=keys["rsa_private"],
rsa_public_key_pem=keys["rsa_public"],
signer_name="test",
)
manifest = unpacker.extract_manifest(nbpf_path)
assert manifest["metadata"]["name"] == "test-plugin"
assert manifest["metadata"]["version"] == "1.0.0"
def test_verify_signature():
"""测试签名验证"""
packer = _create_packer()
unpacker = NBPFUnpacker()
with tempfile.TemporaryDirectory() as tmp:
tmp_path = Path(tmp)
plugin_dir = _create_test_plugin(tmp_path)
keys = _generate_keys(tmp_path)
nbpf_path = tmp_path / "test-plugin.nbpf"
packer.pack(
plugin_dir=plugin_dir,
output_path=nbpf_path,
ed25519_private_key=keys["ed25519_private"],
rsa_private_key_pem=keys["rsa_private"],
rsa_public_key_pem=keys["rsa_public"],
signer_name="test",
)
# 用正确的公钥验证
trusted = {"test": keys["ed25519_public"]}
valid, msg = unpacker.verify_signature(nbpf_path, trusted)
assert valid, f"验证失败: {msg}"
# 用错误的公钥验证
wrong_private, wrong_public = NBPCrypto.generate_ed25519_keypair()
wrong_trusted = {"wrong": wrong_public}
valid, msg = unpacker.verify_signature(nbpf_path, wrong_trusted)
assert not valid, "应该验证失败"
# ═══════════════════════════════════════════════════════════════
# 测试NBPFLoader
# ═══════════════════════════════════════════════════════════════
def test_loader_full_flow():
"""测试加载器完整流程"""
packer = _create_packer()
with tempfile.TemporaryDirectory() as tmp:
tmp_path = Path(tmp)
plugin_dir = _create_test_plugin(tmp_path)
keys = _generate_keys(tmp_path)
# 打包
nbpf_path = tmp_path / "test-plugin.nbpf"
packer.pack(
plugin_dir=plugin_dir,
output_path=nbpf_path,
ed25519_private_key=keys["ed25519_private"],
rsa_private_key_pem=keys["rsa_private"],
rsa_public_key_pem=keys["rsa_public"],
signer_name="test",
)
# 加载
loader = NBPFLoader(
crypto=NBPCrypto(),
compiler=NIRCompiler(),
trusted_ed25519_keys={"test": keys["ed25519_public"]},
trusted_rsa_keys={"test": keys["rsa_public"]},
rsa_private_key=keys["rsa_private"],
)
instance, info = loader.load(nbpf_path)
assert instance is not None
assert info["name"] == "test-plugin"
assert info["version"] == "1.0.0"
# 验证插件功能
assert instance.name == "test-plugin"
assert instance.greet("World") == "Hello, World!"
# 验证启动/停止
assert instance.start() is True
assert instance.started is True
assert instance.stop() is True
assert instance.started is False
def test_loader_wrong_signature():
"""测试加载器拒绝错误签名"""
packer = _create_packer()
with tempfile.TemporaryDirectory() as tmp:
tmp_path = Path(tmp)
plugin_dir = _create_test_plugin(tmp_path)
keys = _generate_keys(tmp_path)
nbpf_path = tmp_path / "test-plugin.nbpf"
packer.pack(
plugin_dir=plugin_dir,
output_path=nbpf_path,
ed25519_private_key=keys["ed25519_private"],
rsa_private_key_pem=keys["rsa_private"],
rsa_public_key_pem=keys["rsa_public"],
signer_name="test",
)
# 用错误的 Ed25519 公钥
_, wrong_public = NBPCrypto.generate_ed25519_keypair()
loader = NBPFLoader(
trusted_ed25519_keys={"wrong": wrong_public},
trusted_rsa_keys={"test": keys["rsa_public"]},
rsa_private_key=keys["rsa_private"],
)
try:
loader.load(nbpf_path)
assert False, "应该抛出 NBPFLoadError"
except NBPFLoadError:
pass
# ═══════════════════════════════════════════════════════════════
# 测试PluginManager 集成
# ═══════════════════════════════════════════════════════════════
def test_plugin_manager_nbpf_methods():
"""测试 PluginManager 的 NBPF 方法"""
from oss.core.engine import PluginManager
pm = PluginManager()
assert hasattr(pm, "load_nbpf")
assert hasattr(pm, "_init_nbpf")
assert pm._nbpf_initialized is False
# 初始化
pm._init_nbpf()
# 没有密钥文件时,加载器可能为 None
# 但方法应该存在且不崩溃
# ═══════════════════════════════════════════════════════════════
# 运行
# ═══════════════════════════════════════════════════════════════
if __name__ == "__main__":
tests = [
("NBPCrypto AES", test_crypto_aes_encrypt_decrypt),
("NBPCrypto 外层加密", test_crypto_outer_encrypt_decrypt),
("NBPCrypto 中层加密", test_crypto_inner_encrypt_decrypt),
("NBPCrypto Ed25519", test_crypto_ed25519_sign_verify),
("NBPCrypto RSA-PSS", test_crypto_rsa_sign_verify),
("NBPCrypto HMAC", test_crypto_hmac_sign_verify),
("NBPCrypto RSA-OAEP", test_crypto_rsa_oaep_key_encapsulation),
("NBPCrypto 反调试", test_crypto_anti_debug),
("NBPCrypto 内存擦除", test_crypto_secure_wipe),
("NIRCompiler 编译", test_compiler_compile_source),
("NIRCompiler 函数创建", test_compiler_create_function),
("NIRCompiler 禁止模块", test_compiler_forbidden_modules),
("NIRCompiler 编译目录", test_compiler_compile_plugin),
("NBPF 打包解包", test_pack_unpack),
("NBPF 提取 manifest", test_extract_manifest),
("NBPF 签名验证", test_verify_signature),
("NBPF 加载器完整流程", test_loader_full_flow),
("NBPF 加载器错误签名", test_loader_wrong_signature),
("PluginManager 集成", test_plugin_manager_nbpf_methods),
]
passed = 0
failed = 0
for name, test_fn in tests:
try:
test_fn()
print(f"{name}")
passed += 1
except Exception as e:
print(f"{name}: {type(e).__name__}: {e}")
failed += 1
print(f"\n结果: {passed} 通过, {failed} 失败, {passed + failed} 总计")
sys.exit(1 if failed > 0 else 0)