Files
Unlucky-Launcher/packaging/ed25519_ref.py
T
AskaEth 918d826645 fix(m190): r1 复审——0x01 修成 \1 + YAML 门禁、被调方声明 secret、拆两个超限测试文件、验签换 ed25519-dalek
四条 P1 + 四条 P2 一并处置(不变的是判据,变的是「谁在承重」)。

P1-1 `.github/workflows/release-build.yml` 的 sed 替换串是裸 0x01(本意 `\1`):GitHub 解析该文件会
失败 ⇒ 整个 run 起不来;即便容忍,版本号也会被求值成单字节,PE 判据报「与 Cargo.toml 不同源?」把
排障引向错方向。修成字面 `\1`,并新增 `packaging/check_workflows_yaml.py`(真 YAML 解析器 parse
全部 workflow + 裸 C0 控制字符字节判据;带 --self-test 注入 0x01/坏结构断言必红),接进
`release-guard.yml` 两步。文本扫描型守卫看不见这类字节,这道门禁专门挡它。

P1-2 `release-publish.yml` 没在 `on.workflow_call.secrets` 里声明 `ULL_MANIFEST_SIGNING_KEY`,调用方
却传了:可复用工作流只拿得到自己声明过的 secret ⇒ 用户配好钥也会在签名步红,而错误文案反过来指控
Secret 没配。补声明(required: false,fail-closed 由步内 ::error:: + exit 1 承担,同
ULL_MSA_CLIENT_ID 的模式)。

P1-3 两个测试支撑文件越过 400 行守卫:签名夹具拆到 `tests/support_sign.rs`(support.rs 458→392),
CLI e2e 的签名部分拆到 `support/update_e2e_sign.rs`(update_e2e.rs 406→348)。纯搬家。

P2-2 「唯一收口点」改成结构事实:从 `UpdateSource` 删掉 `fetch_manifest` 默认实现,调用方只能拿
`verified_manifest(&source, channel)`(源只交原文,想绕过验签就得自己重写解析与渠道判据)。
P2-3 文档写的第 ④ 条命令补进流水线:`sign_manifest.py --verify-pinned` 用 trust.rs 里钉住的公钥
回验刚落盘的 .sig(不碰私钥),发布侧因此验证的是「客户端的判据」而不是「签名脚本的自我声明」。
P2-4 随自写实现一起删除(`Fe` 的错误 PartialEq 不再存在)。
P2-5 app-iced/build.rs 注释里那个 windows_subsystem 属性本分支不存在(M196 才有),改为说明
「本文件只写版本资源,子系统由 rustc 属性决定,check_pe_version.py 会把 subsystem 打出来」。
P2-6 坏清单不再带 error.path 是有意降级(来源 id 在 message 里),保持。

裁定项:自写 Ed25519 换成 `ed25519-dalek::verify_strict`(信任锚不许自写密码学;此刻信任表还是空表,
零兼容负担)。删除 `update/ed25519/{field,group,mod}.rs`(含只服务它们的单测),新 `update/ed25519.rs`
只剩一个薄封装;拒绝类用例(可锻 S+L / 小阶公钥 / 非规范 R / 空签名 / 未知 key)**一条没删**,
改成钉 dalek 的 strict 语义——实测 dalek 对全部五类的结论与旧实现一致(非规范 R 的拒绝理由不同:
dalek 走「小阶点」而非「编码非规范」,用例注释里写明)。签名侧留在 `packaging/ed25519_ref.py`
(纯 Python、零 pip 依赖),`--self-test` 用 RFC 8032 同一组向量钉两边。
2026-11-09 02:14:32 +08:00

143 lines
4.4 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.
"""Ed25519 的**参考实现**(RFC 8032),只给发布侧签名脚本用(M190)。
为什么这里自写一套:发布侧签名必须**零第三方依赖**(CI 不装 pip 包),而仓里 `crates/core` 的验签
用的是 `ed25519-dalek`。两套实现互相独立,`sign_manifest.py --self-test` 用 RFC 8032 §7.1 的同一组
向量把两边各自钉住 —— 任何一边坏了,向量那一半会红;两边都「一致地错」的概率极低。
⚠ 这里的私钥路径(`sign`)**只在发布侧**(`packaging/sign_manifest.py --sign`)跑,不进任何产物;
二进制里只有验签(`ed25519-dalek::verify_strict`)。扩展坐标公式与 dalek/ref10 同源,向量自检兜底。
import ed25519_ref as ref
seed = bytes.fromhex("…") # 32 字节种子
pk = ref.public_key(seed)
sig = ref.sign(seed, b"message") # 64 字节
assert ref.verify(pk, b"message", sig)
"""
from __future__ import annotations
import hashlib
ALG = "ed25519"
Q = 2**255 - 19
L = 2**252 + 27742317777372353535851937790883648493
def _inv(x: int) -> int:
return pow(x, Q - 2, Q)
D = -121665 * _inv(121666) % Q
_D2 = 2 * D % Q
_SQRT_M1 = pow(2, (Q - 1) // 4, Q)
def _recover_x(y: int, sign: int) -> int | None:
u = (y * y - 1) % Q
v = (D * y * y + 1) % Q
v3 = pow(v, 3, Q)
v7 = pow(v, 7, Q)
x = u * v3 % Q * pow(u * v7 % Q, (Q - 5) // 8, Q) % Q
if (v * x * x - u) % Q != 0:
x = x * _SQRT_M1 % Q
if (v * x * x - u) % Q != 0:
return None
if x % 2 != sign:
x = (Q - x) % Q
return x
def _decompress(data: bytes) -> tuple[int, int, int, int] | None:
if len(data) != 32:
return None
y = int.from_bytes(data, "little")
sign = (y >> 255) & 1
y &= (1 << 255) - 1
if y >= Q:
return None
x = _recover_x(y, sign)
if x is None:
return None
return (x, y, 1, x * y % Q)
def _add(p: tuple[int, int, int, int], r: tuple[int, int, int, int]):
x1, y1, z1, t1 = p
x2, y2, z2, t2 = r
a = (y1 - x1) * (y2 - x2) % Q
b = (y1 + x1) * (y2 + x2) % Q
c = t1 * _D2 * t2 % Q
d = 2 * z1 * z2 % Q
e, f, g, h = (b - a) % Q, (d - c) % Q, (d + c) % Q, (b + a) % Q
return (e * f % Q, g * h % Q, f * g % Q, e * h % Q)
def _double(p: tuple[int, int, int, int]):
x1, y1, z1, _ = p
a = x1 * x1 % Q
b = y1 * y1 % Q
c = 2 * z1 * z1 % Q
d = -a % Q
e = ((x1 + y1) * (x1 + y1) - a - b) % Q
g = (d + b) % Q
f = (g - c) % Q
h = (d - b) % Q
return (e * f % Q, g * h % Q, f * g % Q, e * h % Q)
def _scalarmult(p, e: int):
acc = (0, 1, 1, 0)
for bit in bin(e)[2:] if e else "":
acc = _double(acc)
if bit == "1":
acc = _add(acc, p)
return acc
def _encode(p) -> bytes:
x, y, z, _ = p
zi = _inv(z)
x, y = x * zi % Q, y * zi % Q
return (y | ((x & 1) << 255)).to_bytes(32, "little")
def _base():
encoded = (4 * _inv(5) % Q).to_bytes(32, "little")
return _decompress(encoded)
def _hint(data: bytes) -> int:
return int.from_bytes(hashlib.sha512(data).digest(), "little") % L
def public_key(seed: bytes) -> bytes:
h = hashlib.sha512(seed).digest()
a = int.from_bytes(_clamp(bytearray(h[:32])), "little")
return _encode(_scalarmult(_base(), a))
def _clamp(scalar: bytearray) -> bytes:
scalar[0] &= 248
scalar[31] &= 127
scalar[31] |= 64
return bytes(scalar)
def sign(seed: bytes, message: bytes) -> bytes:
if len(seed) != 32:
raise SystemExit("error: Ed25519 种子必须 32 字节")
h = hashlib.sha512(seed).digest()
a = int.from_bytes(_clamp(bytearray(h[:32])), "little")
prefix = h[32:]
r = _hint(prefix + message)
r_encoded = _encode(_scalarmult(_base(), r))
pk = _encode(_scalarmult(_base(), a))
k = _hint(r_encoded + pk + message)
s = (r + k * a) % L
return r_encoded + s.to_bytes(32, "little")
def verify(pk: bytes, message: bytes, signature: bytes) -> bool:
if len(pk) != 32 or len(signature) != 64:
return False
r_bytes, s_bytes = signature[:32], signature[32:]
if int.from_bytes(s_bytes, "little") >= L:
return False
a = _decompress(pk)
r_point = _decompress(r_bytes)
if a is None or r_point is None:
return False
k = _hint(r_bytes + pk + message)
left = _scalarmult(_base(), int.from_bytes(s_bytes, "little"))
return _encode(left) == _encode(_add(r_point, _scalarmult(a, k)))