Files
NebulaShell/oss/core/nbpf/format.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

350 lines
13 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 文件格式定义和打包/解包工具
.nbpf 文件结构ZIP 格式):
```
.nbpf (ZIP)
├── META-INF/
│ ├── MANIFEST.MF # 插件元数据(明文)
│ ├── SIGNATURE # 外层 Ed25519 签名(明文)
│ ├── SIGNER.PEM # 外层签名者公钥(明文)
│ ├── ENCRYPTION # 外层加密信息RSA-OAEP 加密的 AES 密钥1
│ ├── INNER_SIGNATURE # 中层 RSA-4096 签名(加密存储)
│ ├── INNER_ENCRYPTION # 中层加密信息RSA-OAEP 加密的 AES 密钥2
│ └── MODULE_SIGS # 内层 HMAC 签名列表(加密存储)
├── NIR/
│ ├── main # 主模块 NIR双重加密
│ ├── sub_module # 子模块 NIR双重加密
│ └── ...
└── RES/
├── manifest.json # 原始 manifest明文
├── config.py # 配置文件(可选,明文)
├── extensions.py # 扩展配置(可选,明文)
└── ... # 其他资源文件(明文)
```
"""
import json
import zipfile
import io
import os
import hashlib
import base64
from pathlib import Path
from typing import Optional
from oss.logger.logger import Log
from .crypto import NBPCrypto, NBPCryptoError
from .compiler import NIRCompiler, NIRCompileError
class NBPFFormatError(Exception):
""".nbpf 格式错误"""
pass
class NBPFFormatter:
""".nbpf 文件格式常量"""
MAGIC = b"NBPF"
VERSION = 1
ENTRY_POINT = "main"
# ZIP 内部路径
META_INF = "META-INF/"
NIR_DIR = "NIR/"
RES_DIR = "RES/"
# META-INF 文件
MANIFEST = META_INF + "MANIFEST.MF"
SIGNATURE = META_INF + "SIGNATURE"
SIGNER_PEM = META_INF + "SIGNER.PEM"
ENCRYPTION = META_INF + "ENCRYPTION"
INNER_SIGNATURE = META_INF + "INNER_SIGNATURE"
INNER_ENCRYPTION = META_INF + "INNER_ENCRYPTION"
MODULE_SIGS = META_INF + "MODULE_SIGS"
# 跳过列表(打包时排除的文件)
SKIP_FILES = {"__pycache__", "SIGNATURE", ".DS_Store", "Thumbs.db"}
class NBPFPacker:
""".nbpf 打包工具 — 将插件目录打包为 .nbpf 文件"""
def __init__(self, crypto: NBPCrypto = None, compiler: NIRCompiler = None):
self.crypto = crypto or NBPCrypto()
self.compiler = compiler or NIRCompiler()
def pack(
self,
plugin_dir: Path,
output_path: Path,
ed25519_private_key: bytes,
rsa_private_key_pem: bytes,
rsa_public_key_pem: bytes,
ed25519_public_key: bytes = None,
signer_name: str = "unknown",
) -> Path:
"""将插件目录打包为 .nbpf 文件
Args:
plugin_dir: 插件目录路径
output_path: 输出 .nbpf 文件路径
ed25519_private_key: Ed25519 私钥(外层签名)
rsa_private_key_pem: RSA 私钥 PEM中层签名
rsa_public_key_pem: RSA 公钥 PEM用于加密 AES 密钥)
ed25519_public_key: Ed25519 公钥存入包内None 则自动派生)
signer_name: 签名者名称
Returns:
输出文件路径
Raises:
NBPFFormatError: 打包失败
"""
if not plugin_dir.exists():
raise NBPFFormatError(f"插件目录不存在: {plugin_dir}")
# 确保输出目录存在
output_path = Path(output_path)
output_path.parent.mkdir(parents=True, exist_ok=True)
try:
# 1. 读取 manifest
manifest = self._read_manifest(plugin_dir)
# 2. 编译所有 .py 文件为 NIR
Log.info("NBPF", f"编译插件: {plugin_dir.name}")
nir_data = self.compiler.compile_plugin(plugin_dir)
# 3. 收集资源文件
res_files = self._collect_resources(plugin_dir)
# 4. 完整加密打包
Log.info("NBPF", "加密打包中...")
package_info = self.crypto.full_encrypt_package(
nir_data=nir_data,
manifest=manifest,
ed25519_private_key=ed25519_private_key,
rsa_private_key_pem=rsa_private_key_pem,
rsa_public_key_pem=rsa_public_key_pem,
)
# 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"])
# META-INF/SIGNER.PEM
if ed25519_public_key:
zf.writestr(NBPFFormatter.SIGNER_PEM, ed25519_public_key)
else:
# 从私钥派生公钥
ed25519_mod = NBPCrypto._imp_ed25519()
key = ed25519_mod.Ed25519PrivateKey.from_private_bytes(ed25519_private_key)
pub_bytes = key.public_key().public_bytes(
NBPCrypto._imp_serialization().Encoding.Raw,
NBPCrypto._imp_serialization().PublicFormat.Raw
)
zf.writestr(NBPFFormatter.SIGNER_PEM, pub_bytes)
# META-INF/ENCRYPTION
zf.writestr(NBPFFormatter.ENCRYPTION, json.dumps(package_info["outer_encryption"]))
# META-INF/INNER_SIGNATURE
zf.writestr(NBPFFormatter.INNER_SIGNATURE, package_info["inner_signature"])
# META-INF/INNER_ENCRYPTION
zf.writestr(NBPFFormatter.INNER_ENCRYPTION, json.dumps(package_info["inner_encryption"]))
# META-INF/MODULE_SIGS
zf.writestr(NBPFFormatter.MODULE_SIGS, json.dumps(package_info["module_signatures"]))
# NIR/ 目录
for mod_name, enc_info in package_info["inner_encrypted"].items():
nir_path = NBPFFormatter.NIR_DIR + mod_name
zf.writestr(nir_path, json.dumps(enc_info))
# RES/ 目录
for res_path, res_data in res_files.items():
zf.writestr(NBPFFormatter.RES_DIR + res_path, res_data)
Log.ok("NBPF", f"打包完成: {output_path}")
return output_path
except NIRCompileError as e:
raise NBPFFormatError(f"编译失败: {e}") from e
except NBPCryptoError as e:
raise NBPFFormatError(f"加密失败: {e}") from e
except Exception as e:
raise NBPFFormatError(f"打包失败: {type(e).__name__}: {e}") from e
def _read_manifest(self, plugin_dir: Path) -> dict:
"""读取插件 manifest.json"""
manifest_file = plugin_dir / "manifest.json"
if not manifest_file.exists():
# 生成默认 manifest
return {
"metadata": {
"name": plugin_dir.name,
"version": "1.0.0",
"author": "unknown",
"description": "",
},
"config": {"enabled": True, "args": {}},
"dependencies": [],
"permissions": [],
}
try:
return json.loads(manifest_file.read_text(encoding="utf-8"))
except json.JSONDecodeError as e:
raise NBPFFormatError(f"manifest.json 格式错误: {e}") from e
def _collect_resources(self, plugin_dir: Path) -> dict[str, bytes]:
"""收集资源文件(非 .py 文件)"""
resources = {}
for file_path in sorted(plugin_dir.rglob("*")):
if not file_path.is_file():
continue
rel_path = str(file_path.relative_to(plugin_dir))
# 跳过
skip = False
for skip_name in NBPFFormatter.SKIP_FILES:
if skip_name in file_path.parts:
skip = True
break
if skip:
continue
# 跳过 .py 文件(已编译为 NIR
if file_path.suffix == ".py":
continue
# 跳过 manifest.json已单独处理
if file_path.name == "manifest.json":
continue
try:
resources[rel_path] = file_path.read_bytes()
except Exception as e:
Log.warn("NBPF", f"跳过资源文件 {rel_path}: {e}")
return resources
class NBPFUnpacker:
""".nbpf 解包工具 — 解包 .nbpf 文件到目录"""
def __init__(self, crypto: NBPCrypto = None):
self.crypto = crypto or NBPCrypto()
def unpack(self, nbpf_path: Path, output_dir: Path) -> Path:
"""解包 .nbpf 到目录(用于调试/开发)
Args:
nbpf_path: .nbpf 文件路径
output_dir: 输出目录
Returns:
输出目录路径
"""
if not nbpf_path.exists():
raise NBPFFormatError(f".nbpf 文件不存在: {nbpf_path}")
output_dir = Path(output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(nbpf_path, 'r') as zf:
# 提取所有文件
for info in zf.infolist():
# 跳过目录
if info.filename.endswith("/"):
continue
# 计算输出路径
out_path = output_dir / info.filename
# 创建父目录
out_path.parent.mkdir(parents=True, exist_ok=True)
# 写入文件
out_path.write_bytes(zf.read(info.filename))
Log.ok("NBPF", f"解包完成: {output_dir}")
return output_dir
def extract_manifest(self, nbpf_path: Path) -> dict:
"""提取 manifest.json不解密"""
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"))
def verify_signature(
self,
nbpf_path: Path,
trusted_keys: dict[str, bytes],
) -> tuple[bool, str]:
"""验证 .nbpf 文件的外层 Ed25519 签名
签名计算方式与 full_encrypt_package 一致。
Args:
nbpf_path: .nbpf 文件路径
trusted_keys: {signer_name: ed25519_public_key_bytes} 信任的公钥字典
Returns:
(是否通过, 消息)
"""
try:
with zipfile.ZipFile(nbpf_path, 'r') as zf:
# 读取签名和签名者公钥
if NBPFFormatter.SIGNATURE not in zf.namelist():
return False, "缺少 SIGNATURE 文件"
if NBPFFormatter.SIGNER_PEM not in zf.namelist():
return False, "缺少 SIGNER.PEM 文件"
signature_b64 = zf.read(NBPFFormatter.SIGNATURE).decode().strip()
signer_pub_key = zf.read(NBPFFormatter.SIGNER_PEM)
# 查找匹配的信任公钥
matched = False
matched_name = None
for name, trusted_key in trusted_keys.items():
if trusted_key == signer_pub_key:
matched = True
matched_name = name
break
if not matched:
return False, "签名者公钥不在信任列表中"
# 计算包摘要(与 full_encrypt_package 一致)
encryption_data = json.loads(zf.read(NBPFFormatter.ENCRYPTION).decode("utf-8"))
digest = hashlib.sha256()
digest.update(json.dumps(encryption_data["data"]).encode())
# 按模块名排序,添加模块名和密文
nir_modules = {}
for info in zf.infolist():
if info.filename.startswith(NBPFFormatter.NIR_DIR) and not info.filename.endswith("/"):
mod_name = info.filename[len(NBPFFormatter.NIR_DIR):]
mod_data = json.loads(zf.read(info.filename).decode("utf-8"))
nir_modules[mod_name] = mod_data
for mod_name in sorted(nir_modules.keys()):
digest.update(mod_name.encode())
digest.update(nir_modules[mod_name]["ciphertext"].encode())
# 验签
signature = base64.b64decode(signature_b64)
if self.crypto.outer_verify(digest.digest(), signature, signer_pub_key):
return True, f"签名验证通过 (signer: {matched_name})"
else:
return False, "签名验证失败,包可能被篡改"
except Exception as e:
return False, f"签名验证异常: {type(e).__name__}: {e}"