- 盘面图 XSS 防护:装配层全面转义用户可控字段 + 模板严格 CSP(default-src 'none',无 script-src)+ 6 项注入向量回归测试 - 自更新签名信任锚:--apply 双源 manifest 交叉验证 → ed25519 验签(纯 Python 实现,缺 sig 即拒)→ 逐文件 SHA256,全 fail-closed - sync 工具同步后自动重建 manifest + 签名 + 独立验签,私钥缺失 fail-closed - 测试 170 项全绿
153 lines
6.3 KiB
Python
153 lines
6.3 KiB
Python
"""ed25519(RFC 8032)纯 Python 实现与 manifest 签名工具的测试。
|
||
|
||
TEST 1/2/3 为 RFC 8032 §7.1 官方测试向量(空消息/1 字节/2 字节)。
|
||
"""
|
||
import base64
|
||
import json
|
||
import os
|
||
import stat
|
||
import subprocess
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
|
||
ROOT = Path(__file__).resolve().parents[1]
|
||
sys.path.insert(0, str(ROOT / "scripts"))
|
||
import _update_sig as ed
|
||
|
||
# RFC 8032 §7.1:(SECRET KEY, PUBLIC KEY, MESSAGE, SIGNATURE)
|
||
RFC8032_VECTORS = [
|
||
("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60",
|
||
"d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a",
|
||
"",
|
||
"e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e06522490155"
|
||
"5fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b"),
|
||
("4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb",
|
||
"3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c",
|
||
"72",
|
||
"92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da"
|
||
"085ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c00"),
|
||
("c5aa8df43f9f837bedb7442f31dcb7b166d38535076f094b85ce3a2e0b4458f7",
|
||
"fc51cd8e6218a1a38da47ed00230f0580816ed13ba3303ac5deb911548908025",
|
||
"af82",
|
||
"6291d657deec24024827e69c3abe01a30ce548a284743a445e3680d7db5ac3ac"
|
||
"18ff9b538d16f290ae67f760984dc6594a7c15e9716ed28dc027beceea1ec40a"),
|
||
]
|
||
|
||
|
||
class TestRFC8032Vectors:
|
||
@pytest.mark.parametrize("i", range(len(RFC8032_VECTORS)))
|
||
def test_vector(self, i):
|
||
seed, pk, msg, sig = (bytes.fromhex(x) for x in RFC8032_VECTORS[i])
|
||
assert ed.publickey(seed) == pk # 公钥推导
|
||
assert ed.sign(seed, msg) == sig # 确定性签名逐字节一致
|
||
assert ed.verify(pk, msg, sig) is True # 验签通过
|
||
|
||
|
||
class TestSignVerifyRoundtrip:
|
||
def test_roundtrip_random_key(self):
|
||
seed = os.urandom(32)
|
||
msg = '{"version": "9.9.9", "files": {}}'.encode()
|
||
assert ed.verify(ed.publickey(seed), msg, ed.sign(seed, msg)) is True
|
||
|
||
def test_message_one_byte_flip_fails(self):
|
||
seed = os.urandom(32)
|
||
pk = ed.publickey(seed)
|
||
msg = b'{"files": {"a.py": "abc"}}'
|
||
sig = ed.sign(seed, msg)
|
||
bad = bytearray(msg)
|
||
bad[3] ^= 0x01
|
||
assert ed.verify(pk, bytes(bad), sig) is False
|
||
|
||
def test_wrong_public_key_fails(self):
|
||
msg = b"manifest"
|
||
sig = ed.sign(os.urandom(32), msg)
|
||
assert ed.verify(ed.publickey(os.urandom(32)), msg, sig) is False
|
||
|
||
def test_forged_signature_fails(self):
|
||
seed = os.urandom(32)
|
||
pk, msg = ed.publickey(seed), b"manifest"
|
||
sig = bytearray(ed.sign(seed, msg))
|
||
sig[10] ^= 0x01 # 伪造:改 R 的一个字节
|
||
assert ed.verify(pk, msg, bytes(sig)) is False
|
||
assert ed.verify(pk, msg, os.urandom(64)) is False # 全随机签名
|
||
|
||
def test_malformed_inputs_return_false_not_raise(self):
|
||
pk = ed.publickey(os.urandom(32))
|
||
assert ed.verify(pk, b"m", b"") is False # 长度不足
|
||
assert ed.verify(pk, b"m", b"\xff" * 64) is False # S 越界
|
||
assert ed.verify(b"\xff" * 32, b"m", ed.sign(os.urandom(32), b"m")) is False
|
||
assert ed.verify(b"short", b"m", b"x" * 64) is False # 公钥长度错
|
||
sig = bytearray(ed.sign(os.urandom(32), b"m"))
|
||
sig[31] |= 0x80 # R 的 y 坐标 >= 2^255-19 界外/非法点
|
||
sig[0:32] = b"\xff" * 32
|
||
assert ed.verify(pk, b"m", bytes(sig)) is False
|
||
|
||
|
||
SIGN_TOOL = ROOT.parent / "tools" / "sign_manifest.py"
|
||
|
||
|
||
class TestSignManifestTool:
|
||
def _repo(self, tmp_path):
|
||
root = tmp_path / "repo"
|
||
root.mkdir()
|
||
(root / "manifest.json").write_text(
|
||
json.dumps({"version": "9.9.9", "files": {"a.py": "0" * 64}},
|
||
indent=2) + "\n", encoding="utf-8")
|
||
seed = os.urandom(32)
|
||
key = tmp_path / "signing.key"
|
||
key.write_text("# 测试私钥\n# 生成日期:2026-10-10\n" + seed.hex() + "\n",
|
||
encoding="utf-8")
|
||
return root, key, seed
|
||
|
||
def test_sign_and_verify(self, tmp_path):
|
||
root, key, seed = self._repo(tmp_path)
|
||
r = subprocess.run(
|
||
[sys.executable, str(SIGN_TOOL), "--repo-root", str(root),
|
||
"--key", str(key)],
|
||
capture_output=True, text=True, timeout=60)
|
||
assert r.returncode == 0, r.stderr
|
||
sig_text = (root / "manifest.json.sig").read_text(encoding="utf-8")
|
||
assert sig_text.strip() == sig_text.rstrip("\n") and "\n" not in sig_text.strip()
|
||
sig = base64.b64decode(sig_text.strip(), validate=True)
|
||
assert len(sig) == 64
|
||
manifest_bytes = (root / "manifest.json").read_bytes()
|
||
assert ed.verify(ed.publickey(seed), manifest_bytes, sig) is True
|
||
# manifest 改一字节 → 该签名不再有效
|
||
bad = bytearray(manifest_bytes)
|
||
bad[10] ^= 0x01
|
||
assert ed.verify(ed.publickey(seed), bytes(bad), sig) is False
|
||
|
||
def test_missing_manifest_exit_2(self, tmp_path):
|
||
root, key, _ = self._repo(tmp_path)
|
||
(root / "manifest.json").unlink()
|
||
r = subprocess.run(
|
||
[sys.executable, str(SIGN_TOOL), "--repo-root", str(root),
|
||
"--key", str(key)],
|
||
capture_output=True, text=True, timeout=60)
|
||
assert r.returncode == 2
|
||
assert "manifest.json" in r.stderr
|
||
|
||
|
||
class TestRealKeySetup:
|
||
"""作者侧私钥落盘状态:存在、权限 600、绝不进 git 仓库。"""
|
||
|
||
def test_key_file_perms_and_not_in_repo(self):
|
||
key = Path.home() / ".zipec" / "signing.key"
|
||
if not key.exists():
|
||
pytest.skip("作者机上才有私钥")
|
||
assert stat.S_IMODE(key.stat().st_mode) == 0o600
|
||
assert stat.S_IMODE(key.parent.stat().st_mode) == 0o700
|
||
r = subprocess.run(["git", "-C", str(ROOT), "status", "--porcelain"],
|
||
capture_output=True, text=True)
|
||
assert "signing.key" not in r.stdout
|
||
# 私钥对应的公钥 == self_update 内置信任锚
|
||
import self_update as su
|
||
seed = bytes.fromhex(
|
||
[ln for ln in key.read_text().splitlines()
|
||
if ln.strip() and not ln.startswith("#")][0])
|
||
assert ed.publickey(seed).hex() == su.UPDATE_PUBLIC_KEY_HEX
|
||
assert ed.fingerprint(ed.publickey(seed)) == (
|
||
"3089bc1cc9a453e5be66a36574635da6b2ad9797c57af11164111faca157411c")
|