v3.6.1 安全修复同步发版

- 盘面图 XSS 防护:装配层全面转义用户可控字段 + 模板严格 CSP(default-src 'none',无 script-src)+ 6 项注入向量回归测试
- 自更新签名信任锚:--apply 双源 manifest 交叉验证 → ed25519 验签(纯 Python 实现,缺 sig 即拒)→ 逐文件 SHA256,全 fail-closed
- sync 工具同步后自动重建 manifest + 签名 + 独立验签,私钥缺失 fail-closed
- 测试 170 项全绿
This commit is contained in:
2026-10-10 16:21:30 +08:00
parent 433c9b4a41
commit 415cb472b1
13 changed files with 805 additions and 41 deletions
+128
View File
@@ -0,0 +1,128 @@
"""纯 Python ed25519(RFC 8032)签名与验签,零依赖,仅用标准库。
self_update.py --apply 用其中的 verify() 校验 manifest.json 签名;
tools/sign_manifest.py 用 sign() 生成签名。验签性能不在关键路径上
(每次更新只验一次),实现取教科书式的扩展坐标点运算,求正确不求快。
密钥编码:私钥 = 32 字节种子(hex/base64 存储);公钥 = 32 字节 RFC 8032
标准压缩编码;签名 = 64 字节 R‖S。
"""
import hashlib
# 域参数:GF(2^255 - 19),曲线 -x² + y² = 1 + d·x²·y²
_P = 2 ** 255 - 19
_Q = 2 ** 252 + 27742317777372353535851937790883648493 # 群的阶
_D = (-121665 * pow(121666, _P - 2, _P)) % _P
_I = pow(2, (_P - 1) // 4, _P) # sqrt(-1)
def _xrecover(y: int) -> int:
xx = (y * y - 1) * pow(_D * y * y + 1, _P - 2, _P) % _P
x = pow(xx, (_P + 3) // 8, _P)
if (x * x - xx) % _P != 0:
x = x * _I % _P
if x & 1:
x = _P - x
return x
_BY = (4 * pow(5, _P - 2, _P)) % _P
_BX = _xrecover(_BY)
# 基点(扩展坐标 x, y, z, t,x=X/Z, y=Y/Z, t=XY/Z)
_B = (_BX, _BY, 1, _BX * _BY % _P)
_IDENTITY = (0, 1, 1, 0)
def _point_add(P, Q):
x1, y1, z1, t1 = P
x2, y2, z2, t2 = Q
a = (y1 - x1) * (y2 - x2) % _P
b = (y1 + x1) * (y2 + x2) % _P
c = 2 * _D * t1 * t2 % _P
d = 2 * z1 * z2 % _P
e, f, g, h = b - a, d - c, d + c, b + a
return (e * f % _P, g * h % _P, f * g % _P, e * h % _P)
def _scalarmult(P, e: int):
Q = _IDENTITY
while e > 0:
if e & 1:
Q = _point_add(Q, P)
P = _point_add(P, P)
e >>= 1
return Q
def _encode_point(P) -> bytes:
x, y, z, _t = P
zi = pow(z, _P - 2, _P)
x, y = x * zi % _P, y * zi % _P
return (y | ((x & 1) << 255)).to_bytes(32, "little")
def _decode_point(s: bytes):
if len(s) != 32:
raise ValueError("点编码长度须为 32 字节")
y = int.from_bytes(s, "little") & ((1 << 255) - 1)
if y >= _P:
raise ValueError("点编码 y 坐标越界")
x = _xrecover(y)
if (x & 1) != (s[31] >> 7):
x = _P - x
if (-x * x + y * y - 1 - _D * x * x * y * y) % _P != 0:
raise ValueError("点不在曲线上")
return (x, y, 1, x * y % _P)
def _hint(m: bytes) -> int:
return int.from_bytes(hashlib.sha512(m).digest(), "little")
def _secret_expand(seed: bytes):
if len(seed) != 32:
raise ValueError("私钥种子须为 32 字节")
h = hashlib.sha512(seed).digest()
a = int.from_bytes(h[:32], "little")
a &= (1 << 254) - 8
a |= 1 << 254
return a, h[32:]
def publickey(seed: bytes) -> bytes:
"""32 字节种子 → 32 字节公钥。"""
a, _prefix = _secret_expand(seed)
return _encode_point(_scalarmult(_B, a))
def sign(seed: bytes, msg: bytes) -> bytes:
"""RFC 8032 确定性签名:64 字节 R‖S。仅发版工具(tools/)使用。"""
a, prefix = _secret_expand(seed)
A = publickey(seed)
r = _hint(prefix + msg) % _Q
R = _encode_point(_scalarmult(_B, r))
S = (r + _hint(R + A + msg) * a) % _Q
return R + S.to_bytes(32, "little")
def verify(pk: bytes, msg: bytes, sig: bytes) -> bool:
"""验签:通过返回 True;任何不符(含畸形输入)返回 False,不抛异常。"""
if len(pk) != 32 or len(sig) != 64:
return False
R, s = sig[:32], int.from_bytes(sig[32:], "little")
if s >= _Q:
return False
try:
A = _decode_point(pk)
Rpt = _decode_point(R)
except ValueError:
return False
h = _hint(R + pk + msg) % _Q
# [S]B == R + [h]A
return _encode_point(_scalarmult(_B, s)) == _encode_point(
_point_add(Rpt, _scalarmult(A, h)))
def fingerprint(pk: bytes) -> str:
"""公钥指纹:SHA256(公钥) 十六进制,用于注释与审计对照。"""
return hashlib.sha256(pk).hexdigest()