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 同一组向量钉两边。
This commit is contained in:
AskaEth committed 2026-11-09 02:14:32 +08:00
1 parent b7d9b99e12
commit 918d826645
28 files changed
+860 -1085

No files matched your search

+1 -1
View File
@@ -132,7 +132,7 @@ impl<S: UpdateSource> UpdateChecker<S> {
/// 清单取不到、清单不合法、渠道不符都返回 [`UpdateError`]。
/// 「没有更新」不是错误,走 [`CheckOutcome::UpToDate`]。
pub fn check(&self, current: &Version, channel: Channel) -> Result<CheckOutcome, UpdateError> {
let Some(manifest) = self.source.fetch_manifest(channel)? else {
let Some(manifest) = super::source::verified_manifest(&self.source, channel)? else {
return Ok(CheckOutcome::UpToDate {
current: current.clone(),
latest: None,
+50
View File
@@ -0,0 +1,50 @@
//! Ed25519 **严格验签**(`ed25519-dalek::verify_strict`),只验不签、只用公开数据。
//!
//! # 为什么是官方实现,而不是本仓自写
//!
//! M190 第一版为了「scope 里不含 `crates/core/Cargo.toml`」把曲线算术写在了仓里
//! (5x51 素域 + 扩展坐标 + 平方-乘)。r1 复审的裁定是**不允许**:这是**信任锚**,
//! 非规范编码、小阶点、批量验证差异这类微妙错误的后果是「验签通过但本不该通过」,
//! 自写的向量覆盖再全也证明不了「严格性判据选对了」。当时的时机也最好:正式信任表
//! (`trust::PRODUCTION`)还是空表,换实现**零兼容负担**。
//!
//! 于是这里只剩一层薄封装,判据全部交给 dalek:
//!
//! * `VerifyingKey::from_bytes` —— 公钥必须是**规范编码的合法点**(非规范 `y` / 不在曲线上都拒);
//! * `VerifyingKey::verify_strict` —— 比 RFC 8032 的最低要求更严:拒绝**小阶**公钥与小阶 `R`
//! (阻止「谁都能构造自洽四元组」的伪造族),并拒绝**非规范标量 `S`**(`S >= L` 的可锻变体);
//! * `Signature::from_bytes` —— 只搬运 64 字节;真正的判据在 `verify_strict` 里,**不能**换成
//! `verify`(后者不做小阶/可锻检查)。
//!
//! 这批行为由 `tests/ed25519_cases.rs` 逐条钉住(RFC 8032 四条向量 + 六条「该拒就拒」):
//! 那些用例原来用来测自写实现,现在改成**钉 dalek 的 strict 语义**——将来谁把
//! `verify_strict` 换成 `verify`(或换回自写实现),它们会当场红。
//!
//! # 签名侧不在二进制里
//!
//! 本仓没有任何私钥代码:清单由 `packaging/sign_manifest.py`(纯 Python 参考实现,零第三方依赖)
//! 在发布侧签。测试里的签名夹具走 dalek 的 `SigningKey`,只在 `cfg(test)` 里编译。
use ed25519_dalek::{Signature, VerifyingKey};
/// 公钥字节数。
pub(crate) const PUBLIC_KEY_LEN: usize = 32;
/// 签名字节数。
pub(crate) const SIGNATURE_LEN: usize = 64;
/// 验一条 Ed25519 签名(**严格**语义,见模块文档)。
///
/// `message` 是任意长原文——这里恒为清单文件的**原始字节**(见 [`super::signature`])。
#[must_use]
pub(crate) fn verify(
public_key: &[u8; PUBLIC_KEY_LEN],
message: &[u8],
signature: &[u8; SIGNATURE_LEN],
) -> bool {
let Ok(key) = VerifyingKey::from_bytes(public_key) else {
// 公钥都不是规范编码的合法点:直接拒(`from_bytes` 已经做了曲线方程与规范编码检查)。
return false;
};
key.verify_strict(message, &Signature::from_bytes(signature))
.is_ok()
}
-247
View File
@@ -1,247 +0,0 @@
//! 素域 `GF(2^255 - 19)` 的算术:5 条 51 位肢(radix-2^51)+ 128 位中间积。
//!
//! 为什么自己写而不是引 `curve25519-dalek`:M190 的 scope 不含 `crates/core/Cargo.toml`
//! (见 `ed25519/mod.rs` 文件头的取舍说明)。这一层只服务**验签**,不接触任何私钥,
//! 也不需要常数时间性质:输入全是公开数据(公钥、签名、清单字节)。
//!
//! 表示是「宽松的」:`[u64; 5]` 只是数值的内部形态,同一元素有多种写法(肢可以不规范)。
//! 需要规范字节时一律走 [`Fe::to_bytes`](它先进位、再按 `h >= p` 条件减 `p`)。
/// 单肢掩码(51 位)。
const MASK: u64 = (1 << 51) - 1;
/// 域元素。
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct Fe(pub(crate) [u64; 5]);
impl Fe {
/// 0。
pub(crate) const ZERO: Self = Self([0; 5]);
/// 1。
pub(crate) const ONE: Self = Self([1, 0, 0, 0, 0]);
/// 从整数构造(只用于 121665 / 121666 这类小常数)。
pub(crate) fn from_u64(value: u64) -> Self {
Self([value & MASK, value >> 51, 0, 0, 0])
}
/// 读 32 字节小端(不检查是否 `< p`:调用方要自己判定规范性)。
pub(crate) fn from_bytes(bytes: &[u8; 32]) -> Self {
let load = |offset: usize| -> u64 {
let mut buf = [0_u8; 8];
buf.copy_from_slice(&bytes[offset..offset + 8]);
u64::from_le_bytes(buf)
};
// 51 位一段:位 0..51 从字节 0,位 51..102 从字节 6 起右移 3,依此类推。
Self([
load(0) & MASK,
(load(6) >> 3) & MASK,
(load(12) >> 6) & MASK,
(load(19) >> 1) & MASK,
(load(24) >> 12) & MASK,
])
}
/// 规范 32 字节小端编码(先完全进位,再按 `>= p` 条件减一次 `p`)。
pub(crate) fn to_bytes(self) -> [u8; 32] {
let mut h = carry(self.0);
// `q = 1` 当且仅当 `h >= p`:`h + 19` 溢出 2^255 就说明 `h >= 2^255 - 19`。
let mut q = (h[0] + 19) >> 51;
q = (h[1] + q) >> 51;
q = (h[2] + q) >> 51;
q = (h[3] + q) >> 51;
q = (h[4] + q) >> 51;
h[0] += 19 * q;
h[1] += h[0] >> 51;
h[0] &= MASK;
h[2] += h[1] >> 51;
h[1] &= MASK;
h[3] += h[2] >> 51;
h[2] &= MASK;
h[4] += h[3] >> 51;
h[3] &= MASK;
h[4] &= MASK;
let mut out = [0_u8; 32];
let mut acc: u128 = 0;
let mut bits = 0_u32;
let mut at = 0_usize;
for limb in h {
acc |= u128::from(limb) << bits;
bits += 51;
while bits >= 8 {
out[at] = (acc & 0xFF) as u8;
acc >>= 8;
bits -= 8;
at += 1;
}
}
while at < 32 {
out[at] = (acc & 0xFF) as u8;
acc >>= 8;
at += 1;
}
out
}
/// 加法。
pub(crate) fn add(&self, rhs: &Self) -> Self {
let f = self.0;
let g = rhs.0;
Self(carry([
f[0] + g[0],
f[1] + g[1],
f[2] + g[2],
f[3] + g[3],
f[4] + g[4],
]))
}
/// 减法(加 `2p` 再减,避免借位)。
pub(crate) fn sub(&self, rhs: &Self) -> Self {
let f = self.0;
let g = rhs.0;
// 2p 的肢:`2*(2^51-19)` 与 `2*(2^51-1)`。
let two_p0 = 0x000F_FFFF_FFFF_FFDA;
let two_p = 0x000F_FFFF_FFFF_FFFE;
Self(carry([
f[0] + two_p0 - g[0],
f[1] + two_p - g[1],
f[2] + two_p - g[2],
f[3] + two_p - g[3],
f[4] + two_p - g[4],
]))
}
/// 取负。
pub(crate) fn neg(&self) -> Self {
Self::ZERO.sub(self)
}
/// 乘法(教科书式 5x5,中间积用 128 位)。
pub(crate) fn mul(&self, rhs: &Self) -> Self {
let f = self.0;
let g = rhs.0;
let g1 = u128::from(g[1]) * 19;
let g2 = u128::from(g[2]) * 19;
let g3 = u128::from(g[3]) * 19;
let g4 = u128::from(g[4]) * 19;
let f0 = u128::from(f[0]);
let f1 = u128::from(f[1]);
let f2 = u128::from(f[2]);
let f3 = u128::from(f[3]);
let f4 = u128::from(f[4]);
let g0 = u128::from(g[0]);
let mut h0 = f0 * g0 + f1 * g4 + f2 * g3 + f3 * g2 + f4 * g1;
let mut h1 = f0 * u128::from(g[1]) + f1 * g0 + f2 * g4 + f3 * g3 + f4 * g2;
let mut h2 = f0 * u128::from(g[2]) + f1 * u128::from(g[1]) + f2 * g0 + f3 * g4 + f4 * g3;
let mut h3 = f0 * u128::from(g[3])
+ f1 * u128::from(g[2])
+ f2 * u128::from(g[1])
+ f3 * g0
+ f4 * g4;
let mut h4 = f0 * u128::from(g[4])
+ f1 * u128::from(g[3])
+ f2 * u128::from(g[2])
+ f3 * u128::from(g[1])
+ f4 * g0;
let mask128 = u128::from(MASK);
let mut c = h0 >> 51;
h0 &= mask128;
h1 += c;
c = h1 >> 51;
h1 &= mask128;
h2 += c;
c = h2 >> 51;
h2 &= mask128;
h3 += c;
c = h3 >> 51;
h3 &= mask128;
h4 += c;
c = h4 >> 51;
h4 &= mask128;
h0 += c * 19;
c = h0 >> 51;
h0 &= mask128;
h1 += c;
Self([h0 as u64, h1 as u64, h2 as u64, h3 as u64, h4 as u64])
}
/// 平方(乘法的特化,只为可读性)。
pub(crate) fn square(&self) -> Self {
self.mul(self)
}
/// `self^exponent`,指数以 32 字节小端给出(从最高位往下平方-乘)。
pub(crate) fn pow(&self, exponent: &[u8; 32]) -> Self {
let mut acc = Self::ONE;
for byte_index in (0..32).rev() {
for bit in (0..8).rev() {
acc = acc.square();
if (exponent[byte_index] >> bit) & 1 == 1 {
acc = acc.mul(self);
}
}
}
acc
}
/// 乘法逆元(`p` 是素数,故 `x^(p-2)`)。
pub(crate) fn invert(&self) -> Self {
// p - 2 = 2^255 - 21
let mut e = [0xFF_u8; 32];
e[0] = 0xEB;
e[31] = 0x7F;
self.pow(&e)
}
/// `x^((p-5)/8)`,开方用(`(p-5)/8 = 2^252 - 3`)。
pub(crate) fn pow_p58(&self) -> Self {
let mut e = [0xFF_u8; 32];
e[0] = 0xFD;
e[31] = 0x0F;
self.pow(&e)
}
/// `sqrt(-1) = 2^((p-1)/4)`(`p ≡ 5 mod 8` 时开方失败要用它翻一次符号)。
pub(crate) fn sqrt_m1() -> Self {
// (p-1)/4 = 2^253 - 5
let mut e = [0xFF_u8; 32];
e[0] = 0xFB;
e[31] = 0x1F;
Self::from_u64(2).pow(&e)
}
/// 最低位(规范编码的第 0 位);点解压时与签名的符号位比对。
pub(crate) fn is_odd(&self) -> bool {
self.to_bytes()[0] & 1 == 1
}
/// 是否等于 0(按规范编码比)。
pub(crate) fn is_zero(&self) -> bool {
self.to_bytes() == [0_u8; 32]
}
}
/// 把肢挽到 51 位以内(高位的溢出折回第 0 肢,因为 `2^255 ≡ 19`)。
fn carry(mut h: [u64; 5]) -> [u64; 5] {
let mut c = h[0] >> 51;
h[0] &= MASK;
h[1] += c;
c = h[1] >> 51;
h[1] &= MASK;
h[2] += c;
c = h[2] >> 51;
h[2] &= MASK;
h[3] += c;
c = h[3] >> 51;
h[3] &= MASK;
h[4] += c;
c = h[4] >> 51;
h[4] &= MASK;
h[0] += c * 19;
c = h[0] >> 51;
h[0] &= MASK;
h[1] += c;
h
}
-168
View File
@@ -1,168 +0,0 @@
//! 扭曲爱德华兹曲线上的群运算(`a = -1`,扩展坐标 `(X:Y:Z:T)`,`xy = T/Z`)。
//!
//! 只用两条公式(Hisil-Wong-Carter-Dawson 2008):加法 `add-2008-hwcd-3`、倍点 `dbl-2008-hwcd`,
//! 以及一个最朴素的「从最高位往下的平方-乘」标量乘。验签一次只要两次标量乘,朴素实现足够,
//! 换来的是每一步都能对着公式读——这里宁可慢一点也不要一个看不懂的窗口表。
use std::sync::OnceLock;
use super::field::Fe;
/// 曲线常数 `d = -121665/121666`。
fn d() -> &'static Fe {
static D: OnceLock<Fe> = OnceLock::new();
D.get_or_init(|| {
Fe::from_u64(121665)
.mul(&Fe::from_u64(121666).invert())
.neg()
})
}
/// `2d`(加法公式里那一步)。
fn d2() -> &'static Fe {
static D2: OnceLock<Fe> = OnceLock::new();
D2.get_or_init(|| d().add(d()))
}
/// 基点 `B` 的压缩编码(RFC 8032 §5.1 的标准常量,解压即得基点)。
const BASE_ENCODING: [u8; 32] = [
0x58, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
];
/// 曲线上的点(扩展坐标)。
#[derive(Clone, Copy, Debug)]
pub(crate) struct Point {
x: Fe,
y: Fe,
z: Fe,
t: Fe,
}
impl Point {
/// 单位元 `(0, 1, 1, 0)`。
pub(crate) fn identity() -> Self {
Self {
x: Fe::ZERO,
y: Fe::ONE,
z: Fe::ONE,
t: Fe::ZERO,
}
}
/// 基点 `B`。
pub(crate) fn base() -> Self {
// 常量本身是合法压缩点;万一实现坏了这一步会返回 None,用身份元兜底并让上层验签必然失败。
Self::decompress(&BASE_ENCODING).unwrap_or_else(Self::identity)
}
/// 点加。
pub(crate) fn add(&self, rhs: &Self) -> Self {
let a = self.y.sub(&self.x).mul(&rhs.y.sub(&rhs.x));
let b = self.y.add(&self.x).mul(&rhs.y.add(&rhs.x));
let c = self.t.mul(&d2().mul(&rhs.t));
let dd = self.z.mul(&rhs.z);
let dd = dd.add(&dd);
let e = b.sub(&a);
let f = dd.sub(&c);
let g = dd.add(&c);
let h = b.add(&a);
Self {
x: e.mul(&f),
y: g.mul(&h),
t: e.mul(&h),
z: f.mul(&g),
}
}
/// 倍点。
pub(crate) fn double(&self) -> Self {
let a = self.x.square();
let b = self.y.square();
let z2 = self.z.square();
let c = z2.add(&z2);
let dd = a.neg();
let e = self.x.add(&self.y).square().sub(&a).sub(&b);
let g = dd.add(&b);
let f = g.sub(&c);
let h = dd.sub(&b);
Self {
x: e.mul(&f),
y: g.mul(&h),
t: e.mul(&h),
z: f.mul(&g),
}
}
/// 压缩成 32 字节(`y` 的规范编码 + `x` 的最低位放进第 255 位)。
pub(crate) fn compress(&self) -> [u8; 32] {
let z_inv = self.z.invert();
let x = self.x.mul(&z_inv);
let y = self.y.mul(&z_inv);
let mut out = y.to_bytes();
if x.is_odd() {
out[31] |= 0x80;
}
out
}
/// 从 32 字节压缩编码解压;**严格**:非规范的 `y`、解不出 `x`、`x = 0` 却带符号位都返回 `None`。
pub(crate) fn decompress(bytes: &[u8; 32]) -> Option<Self> {
let mut raw = *bytes;
let sign = raw[31] >> 7;
raw[31] &= 0x7F;
let y = Fe::from_bytes(&raw);
// 规范编码才收:`y >= p` 的写法(同一元素的第二种编法)一律拒绝,防「同一签名两种字节」。
if y.to_bytes() != raw {
return None;
}
// x^2 = (y^2 - 1) / (d y^2 + 1)
let y2 = y.square();
let u = y2.sub(&Fe::ONE);
let v = d().mul(&y2).add(&Fe::ONE);
let v3 = v.square().mul(&v);
let v7 = v3.square().mul(&v);
let mut x = u.mul(&v3).mul(&u.mul(&v7).pow_p58());
if v.mul(&x.square()).to_bytes() != u.to_bytes() {
// p ≡ 5 mod 8:这一支解不出时,乘 sqrt(-1) 再试一次(另一半平方根)。
x = x.mul(&Fe::sqrt_m1());
if v.mul(&x.square()).to_bytes() != u.to_bytes() {
return None;
}
}
if x.is_zero() {
// x = 0 时符号位只能是 0(否则就是「-0」,非规范)。
if sign == 1 {
return None;
}
} else if x.is_odd() != (sign == 1) {
x = x.neg();
}
Some(Self {
x,
y,
z: Fe::ONE,
t: x.mul(&y),
})
}
/// `[scalar] self`,标量以 32 字节小端给出(从最高位往下:倍点,置位才加)。
pub(crate) fn mul(&self, scalar: &[u8; 32]) -> Self {
let mut acc = Self::identity();
for byte_index in (0..32).rev() {
for bit in (0..8).rev() {
acc = acc.double();
if (scalar[byte_index] >> bit) & 1 == 1 {
acc = acc.add(self);
}
}
}
acc
}
/// 是否小阶点(`[8]P` 是单位元就是;`verify_strict` 的第一道严格性判据)。
pub(crate) fn is_small_order(&self) -> bool {
let p8 = self.double().double().double();
p8.x.is_zero() && p8.y.to_bytes() == p8.z.to_bytes()
}
}
-265
View File
@@ -1,265 +0,0 @@
//! Ed25519 **严格验签**(RFC 8032 §5.1.7),只验不签、只用公开数据。
//!
//! # 为什么这里没有引 `curve25519-dalek` / `ed25519-dalek`
//!
//! M190 的 scope 不含 `crates/core/Cargo.toml`,而清单验签必须在 `core::update` 里(GUI 与 CLI
//! 共用 `Job`,验签只能有一个收口点)。取舍:把验签实现**就地**写成三个文件(`field` 素域算术 /
//! `group` 曲线运算 / 本文件判据),零新依赖,判据对着 RFC 8032 的官方测试向量逐条验
//! (`tests/ed25519_cases.rs`)。
//!
//! 这条取舍的代价必须说清:
//!
//! * 实现只有「验」这一半——没有私钥、没有 nonce、没有常数时间要求,攻击面比完整签名库小得多;
//! * 但它是**自写密码学**。日后 scope 允许时应当整体换成 `ed25519-dalek` 的
//! `verify_strict`,对外只留 [`verify`] 一个函数,替换是改这一个函数体;
//! * 严格性按 `verify_strict` 的口径来:拒绝非规范编码(`y >= p`、`-0`)、拒绝小阶公钥与小阶 `R`、
//! 拒绝非规范标量 `S`(`S >= L`,即可锻性)。少任何一条都能造出「改一个字节仍通过」的假签。
//!
//! # 判据(逐条,都能单独变红)
//!
//! ```text
//! ① 解 A、解 R 都必须成功且是规范编码 (非规范编码 → 拒)
//! ② A、R 都不能是小阶点 (小阶 → 拒)
//! ③ S < L(L = 2^252 + 27742317777372353535851937790883648493)
//! ④ k = SHA-512(R‖A‖M) mod L
//! ⑤ [S]B == R + [k]A(压缩后逐字节比)
//! ```
mod field;
mod group;
use sha2::{Digest, Sha512};
use self::group::Point;
/// 公钥字节数。
pub(crate) const PUBLIC_KEY_LEN: usize = 32;
/// 签名字节数。
pub(crate) const SIGNATURE_LEN: usize = 64;
/// `L = 2^252 + 27742317777372353535851937790883648493`(小端 4 条 64 位肢)。
const L: [u64; 4] = [
0x5812_631a_5cf5_d3ed,
0x14de_f9de_a2f7_9cd6,
0x0000_0000_0000_0000,
0x1000_0000_0000_0000,
];
/// 验一条 Ed25519 签名。
///
/// `public_key` 与 `signature` 的语义就是 RFC 8032 的那两段字节;`message` 是任意长原文
/// (这里恒为清单文件的原始字节,见 [`super::signature`])。
#[must_use]
pub(crate) fn verify(
public_key: &[u8; PUBLIC_KEY_LEN],
message: &[u8],
signature: &[u8; SIGNATURE_LEN],
) -> bool {
let mut r_bytes = [0_u8; PUBLIC_KEY_LEN];
r_bytes.copy_from_slice(&signature[..PUBLIC_KEY_LEN]);
let mut s_bytes = [0_u8; PUBLIC_KEY_LEN];
s_bytes.copy_from_slice(&signature[PUBLIC_KEY_LEN..]);
// ③ S 必须是规范标量:`S >= L` 时同一个「消息 + 公钥」存在多个可验证的签名,签名的不可锻性就没了。
if !scalar_is_canonical(&s_bytes) {
return false;
}
let Some(public_point) = Point::decompress(public_key) else {
return false;
};
let Some(r_point) = Point::decompress(&r_bytes) else {
return false;
};
// ② 小阶点会让「谁都能构造一个自洽的四元组」,严格验签一律拒。
if public_point.is_small_order() || r_point.is_small_order() {
return false;
}
// ④ k = SHA-512(R ‖ A ‖ M) mod L
let mut hasher = Sha512::new();
hasher.update(r_bytes);
hasher.update(public_key);
hasher.update(message);
let digest = hasher.finalize();
let mut wide = [0_u8; 64];
wide.copy_from_slice(&digest);
let k = reduce_512(&wide);
// ⑤ [S]B 与 R + [k]A 必须是同一个点(压缩后逐字节比即「同一个点」,因为压缩是单射)。
let left = Point::base().mul(&s_bytes);
let right = r_point.add(&public_point.mul(&k));
left.compress() == right.compress()
}
/// 从 32 字节种子推出公钥(**只给测试用**:正式构建的验签不需要它,也没有任何私钥代码)。
#[cfg(test)]
pub(crate) fn public_key_from_seed_for_tests(seed: &[u8; 32]) -> [u8; PUBLIC_KEY_LEN] {
let (scalar, _) = expand_seed(seed);
Point::base().mul(&scalar).compress()
}
/// 用 32 字节种子签一条消息(**只给测试用**,见 [`public_key_from_seed_for_tests`])。
#[cfg(test)]
pub(crate) fn sign_for_tests(seed: &[u8; 32], message: &[u8]) -> [u8; SIGNATURE_LEN] {
let (scalar, prefix) = expand_seed(seed);
let public_key = Point::base().mul(&scalar).compress();
let mut hasher = Sha512::new();
hasher.update(prefix);
hasher.update(message);
let mut wide = [0_u8; 64];
wide.copy_from_slice(&hasher.finalize());
let r = reduce_512(&wide);
let r_encoded = Point::base().mul(&r).compress();
let mut hasher = Sha512::new();
hasher.update(r_encoded);
hasher.update(public_key);
hasher.update(message);
let mut wide = [0_u8; 64];
wide.copy_from_slice(&hasher.finalize());
let k = reduce_512(&wide);
let s = scalar_mul_add_mod_l(&k, &scalar, &r);
let mut out = [0_u8; SIGNATURE_LEN];
out[..PUBLIC_KEY_LEN].copy_from_slice(&r_encoded);
out[PUBLIC_KEY_LEN..].copy_from_slice(&s);
out
}
/// RFC 8032 §5.1.5 的种子展开:`SHA-512(seed)` 的前半是按位裁剪的标量、后半是 nonce 前缀。
#[cfg(test)]
fn expand_seed(seed: &[u8; 32]) -> ([u8; 32], [u8; 32]) {
let digest = Sha512::digest(seed);
let mut scalar = [0_u8; 32];
scalar.copy_from_slice(&digest[..32]);
scalar[0] &= 248;
scalar[31] &= 127;
scalar[31] |= 64;
let mut prefix = [0_u8; 32];
prefix.copy_from_slice(&digest[32..]);
(scalar, prefix)
}
/// `S < L` 判定(小端 32 字节)。
fn scalar_is_canonical(bytes: &[u8; 32]) -> bool {
let mut limbs = [0_u64; 4];
let (chunks, _) = bytes.as_chunks::<8>();
for (limb, chunk) in limbs.iter_mut().zip(chunks) {
*limb = u64::from_le_bytes(*chunk);
}
// 从最高肢往下比:高于 L 的最高肢就不可能小于 L。
for index in (0..4).rev() {
if limbs[index] < L[index] {
return true;
}
if limbs[index] > L[index] {
return false;
}
}
false
}
/// 把一个 512 位大端-无关的字节串按小端解释,对 `L` 取模。
///
/// `bytes[63]` 是最高字节。做法是最朴素的竖式:每次左移 8 位再补一个字节,随后反复减 `L`
/// 直到小于 `L`。一次验签跑两遍、每遍最坏 64*256 次减——比任何花招都更容易对着看懂。
fn reduce_512(bytes: &[u8; 64]) -> [u8; 32] {
let mut acc = [0_u64; 5];
for byte in bytes.iter().rev() {
let mut carry = u128::from(*byte);
for limb in &mut acc {
let wide = (u128::from(*limb) << 8) | carry;
*limb = wide as u64;
carry = wide >> 64;
}
while !below_l(&acc) {
subtract_l(&mut acc);
}
}
let mut out = [0_u8; 32];
for index in 0..4 {
out[index * 8..index * 8 + 8].copy_from_slice(&acc[index].to_le_bytes());
}
out
}
/// `acc < L`(`acc` 是 5 肢,最高肢非零即「远大于 L」)。
fn below_l(acc: &[u64; 5]) -> bool {
if acc[4] != 0 {
return false;
}
for index in (0..4).rev() {
if acc[index] < L[index] {
return true;
}
if acc[index] > L[index] {
return false;
}
}
false
}
/// `acc -= L`(调用方保证 `acc >= L`)。
fn subtract_l(acc: &mut [u64; 5]) {
let mut borrow = 0_u64;
for index in 0..4 {
let (value, first) = acc[index].overflowing_sub(L[index]);
let (value, second) = value.overflowing_sub(borrow);
acc[index] = value;
borrow = u64::from(first || second);
}
let (value, _) = acc[4].overflowing_sub(borrow);
acc[4] = value;
}
/// `(k*a + r) mod L`(测试签名用;`k`、`a`、`r` 都已是规范标量)。
#[cfg(test)]
fn scalar_mul_add_mod_l(k: &[u8; 32], a: &[u8; 32], r: &[u8; 32]) -> [u8; 32] {
// 256x256 -> 512 位乘积:8 条 64 位肢,逐肢进位后再按字节展开成 64 字节(小端)。
let mut product = [0_u64; 8];
for (left_index, left) in limbs_of(k).iter().enumerate() {
let mut carry = 0_u128;
for (right_index, right) in limbs_of(a).iter().enumerate() {
let cell = u128::from(*left) * u128::from(*right)
+ u128::from(product[left_index + right_index])
+ carry;
product[left_index + right_index] = cell as u64;
carry = cell >> 64;
}
product[left_index + 4] = u64::try_from(carry).unwrap_or(0);
}
let mut wide = [0_u8; 64];
for (index, limb) in product.iter().enumerate() {
wide[index * 8..index * 8 + 8].copy_from_slice(&limb.to_le_bytes());
}
let reduced = reduce_512(&wide);
// 加上 r 再取模一次(`acc` 5 肢 + 反复减 L,与 reduce_512 同一手法)。
let mut acc = [0_u64; 5];
for index in 0..4 {
let mut buf = [0_u8; 8];
buf.copy_from_slice(&reduced[index * 8..index * 8 + 8]);
acc[index] = u64::from_le_bytes(buf);
}
let mut carry = 0_u64;
for (slot, rhs) in acc.iter_mut().zip(limbs_of(r)) {
let (sum, overflow) = slot.overflowing_add(rhs);
let (sum, overflow2) = sum.overflowing_add(carry);
*slot = sum;
carry = u64::from(overflow || overflow2);
}
acc[4] = carry;
while !below_l(&acc) {
subtract_l(&mut acc);
}
let mut out = [0_u8; 32];
for index in 0..4 {
out[index * 8..index * 8 + 8].copy_from_slice(&acc[index].to_le_bytes());
}
out
}
/// 32 字节小端 -> 4 条 64 位肢。
#[cfg(test)]
fn limbs_of(bytes: &[u8; 32]) -> [u64; 4] {
let mut limbs = [0_u64; 4];
let (chunks, _) = bytes.as_chunks::<8>();
for (limb, chunk) in limbs.iter_mut().zip(chunks) {
*limb = u64::from_le_bytes(*chunk);
}
limbs
}
+1 -1
View File
@@ -31,7 +31,7 @@
//! source UpdateSource trait + 本地目录源 github GitHub Releases 源(D50)
//! verify 暂存 + 长度/摘要校验 checker 选择哪一份发布、急不急
//! signature 清单签名信封 + 验签 trust 钉进二进制的公钥表(keyring)
//! ed25519 Ed25519 严格验签(自写,无新依赖)
//! ed25519 Ed25519 严格验签(ed25519-dalek::verify_strict 的薄封装)
//! install 原子替换 + 回滚 replace_io 改名/复制/重试与跨卷判据(注入点)
//! error 分类与文案 job 检查/应用的一条龙(GUI 与 CLI 共用同一入口)
//! ```
+4 -12
View File
@@ -68,22 +68,14 @@ pub trait UpdateSource: Send + Sync {
///
/// 源里没有这个文件、读失败、写 `dest` 失败都返回 [`UpdateError`]。
fn fetch_artifact(&self, artifact: &Artifact, dest: &Path) -> Result<u64, UpdateError>;
/// 取清单 → **验签** → 解析 → 渠道校验。`check`/`apply` 都只走这一个入口。
///
/// 提供默认实现是为了让「怎么取字节」与「怎么判字节」彻底分开:源只实现
/// [`Self::fetch_manifest_documents`],判据只有 [`verified_manifest`] 一份。
///
/// # Errors
///
/// 取不到/取到空、签名不通过(五类,见 [`UpdateErrorKind`])、清单 JSON 坏、渠道不符。
fn fetch_manifest(&self, channel: Channel) -> Result<Option<ReleaseManifest>, UpdateError> {
verified_manifest(self, channel)
}
}
/// 取清单 → 验签 → 解析 → 渠道校验(**唯一**入口,GUI 与 CLI 共用)。
///
/// 它是个**自由函数**而不是 trait 的默认方法:trait 方法可被任何实现方覆写,那样「信任根的收口点」
/// 就只剩纪律(M190 r1 复审 P2-2)。删掉那条默认实现之后,调用方手里只有源交出来的**原文**
/// ([`ManifestDocuments`]),想绕过验签就必须自己重写一遍解析 + 渠道判据 —— 不再是「一行覆写」。
///
/// # Errors
///
/// 源里没有该渠道清单 ⇒ `Ok(None)`;签名/解析/渠道任一不过 ⇒ [`UpdateError`]。
+106 -75
View File
@@ -1,12 +1,23 @@
//! Ed25519 严格验签的判据(RFC 8032 §7.1 官方测试向量 + 六条「该拒就拒」)。
//! Ed25519 验签判据:**钉住 `ed25519-dalek::verify_strict` 的行为**(RFC 8032 §7.1 向量 + 六条拒绝类)。
//!
//! 这一组用例是自写密码学的**唯一**公信力来源:向量来自 RFC 附录(不是自己签自己验),
//! 拒绝类用例覆盖「改一个字节仍然通过」的全部已知构造(改 R / 改 S / S += L / 小阶公钥 /
//! 非规范 R 编码 / 空签名)。
//! M190 r1 复审把自写曲线算术整体换成了官方实现(理由见 `crate::update::ed25519` 的模块文档)。
//! 这批用例的定位因此从「测自己的实现」变成「**钉 dalek 的严格语义**」:
//!
//! | 用例 | 它挡住什么 |
//! |---|---|
//! | `rfc8032_vectors_verify` | 实现换成任何一个错的版本(向量是 RFC 附录的,不是自签自验) |
//! | `malleable_s_plus_l_is_rejected` | 把 `verify_strict` 换成 `verify`(后者不检查 `S` 规范性) |
//! | `small_order_public_key_is_rejected` | 同上:小阶公钥/小阶 `R` 在 `verify` 下能「自洽通过」 |
//! | `non_canonical_r_encoding_is_rejected` | 非规范编码的点(`y >= p` 的第二种写法) |
//! | `empty_signature_is_rejected` / `message_bit_flip` / `signature_bit_flip` | 判据本身失效(恒真) |
//! | `signing_key_round_trips` | 测试夹具(dalek `SigningKey`)与验签侧是同一套语义 |
//!
//! 每条都实测过 dalek 2.2 的**实际**行为;与「RFC 最低要求」不同之处在用例注释里写明(见
//! `non_canonical_r_encoding_is_rejected`:dalek 拒它的理由是**小阶**,不是「非规范编码」单独一条)。
use crate::update::ed25519::{
public_key_from_seed_for_tests, sign_for_tests, verify, PUBLIC_KEY_LEN, SIGNATURE_LEN,
};
use crate::update::ed25519::{verify, PUBLIC_KEY_LEN, SIGNATURE_LEN};
use super::support_sign::{hex_of, public_key_hex, sign_bytes};
/// 十六进制转字节(长度不对/字符非法当场 panic:这些向量是写死的常量)。
fn unhex(text: &str) -> Vec<u8> {
@@ -15,15 +26,15 @@ fn unhex(text: &str) -> Vec<u8> {
cleaned.len().is_multiple_of(2),
"十六进制串长度必须是偶数:{text}"
);
cleaned
.as_bytes()
.chunks(2)
.map(|pair| {
let hi = (pair[0] as char).to_digit(16).expect("十六进制字符");
let lo = (pair[1] as char).to_digit(16).expect("十六进制字符");
u8::try_from(hi * 16 + lo).expect("字节")
})
.collect()
let bytes = cleaned.as_bytes();
let mut out = Vec::with_capacity(bytes.len() / 2);
let (pairs, _) = bytes.as_chunks::<2>();
for pair in pairs {
let hi = (pair[0] as char).to_digit(16).expect("十六进制字符");
let lo = (pair[1] as char).to_digit(16).expect("十六进制字符");
out.push(u8::try_from(hi * 16 + lo).expect("字节"));
}
out
}
fn array32(bytes: &[u8]) -> [u8; 32] {
@@ -44,28 +55,28 @@ const L_BYTES: [u8; 32] = [
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10,
];
/// RFC 8032 的四个向量:(public, message, signature)。
fn rfc_vectors() -> Vec<(String, String, String)> {
/// RFC 8032 §7.1 的四条向量:(public, message, signature)。
fn rfc_vectors() -> Vec<(&'static str, &'static str, &'static str)> {
vec![
(
"d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a".to_string(),
String::new(),
"e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b".to_string(),
"d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a",
"",
"e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b",
),
(
"3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c".to_string(),
"72".to_string(),
"92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da085ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c00".to_string(),
"3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c",
"72",
"92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da085ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c00",
),
(
"fc51cd8e6218a1a38da47ed00230f0580816ed13ba3303ac5deb911548908025".to_string(),
"af82".to_string(),
"6291d657deec24024827e69c3abe01a30ce548a284743a445e3680d7db5ac3ac18ff9b538d16f290ae67f760984dc6594a7c15e9716ed28dc027beceea1ec40a".to_string(),
"fc51cd8e6218a1a38da47ed00230f0580816ed13ba3303ac5deb911548908025",
"af82",
"6291d657deec24024827e69c3abe01a30ce548a284743a445e3680d7db5ac3ac18ff9b538d16f290ae67f760984dc6594a7c15e9716ed28dc027beceea1ec40a",
),
(
"ec172b93ad5e563bf4932c70e1245034c35467ef2efd4d64ebf819683467e2bf".to_string(),
"ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f".to_string(),
"dc2a4459e7369633a52b1bf277839a00201009a3efbf3ecb69bea2186c26b58909351fc9ac90b3ecfdfbc7c66431e0303dca179c138ac17ad9bef1177331a704".to_string(),
"ec172b93ad5e563bf4932c70e1245034c35467ef2efd4d64ebf819683467e2bf",
"ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f",
"dc2a4459e7369633a52b1bf277839a00201009a3efbf3ecb69bea2186c26b58909351fc9ac90b3ecfdfbc7c66431e0303dca179c138ac17ad9bef1177331a704",
),
]
}
@@ -73,11 +84,12 @@ fn rfc_vectors() -> Vec<(String, String, String)> {
#[test]
fn rfc8032_vectors_verify() {
for (index, (public, message, signature)) in rfc_vectors().into_iter().enumerate() {
let public = array32(&unhex(&public));
let message = unhex(&message);
let signature = array64(&unhex(&signature));
assert!(
verify(&public, &message, &signature),
verify(
&array32(&unhex(public)),
&unhex(message),
&array64(&unhex(signature))
),
"RFC 8032 第 {} 条向量必须通过",
index + 1
);
@@ -86,37 +98,41 @@ fn rfc8032_vectors_verify() {
#[test]
fn message_bit_flip_is_rejected() {
let (public, message, signature) = &rfc_vectors()[1];
let public = array32(&unhex(public));
let (public, message, signature) = rfc_vectors()[1];
let mut message = unhex(message);
message[0] ^= 0x01;
let signature = array64(&unhex(signature));
assert!(
!verify(&public, &message, &signature),
!verify(
&array32(&unhex(public)),
&message,
&array64(&unhex(signature))
),
"改一个消息字节必须被拒"
);
}
#[test]
fn signature_bit_flip_is_rejected() {
let (public, message, signature) = &rfc_vectors()[2];
let public = array32(&unhex(public));
let message = unhex(message);
let mut signature = array64(&unhex(signature));
// 只改 R 的最后一位(不动消息、不动 S):证明「只改签名字节」也会红。
signature[31] ^= 0x01;
assert!(!verify(&public, &message, &signature), "改 R 必须被拒");
let mut signature = array64(&unhex(&rfc_vectors()[2].2));
signature[63] ^= 0x04;
assert!(!verify(&public, &message, &signature), "改 S 必须被拒");
let (public, message, signature) = rfc_vectors()[2];
let mut r_flipped = array64(&unhex(signature));
r_flipped[31] ^= 0x01;
assert!(
!verify(&array32(&unhex(public)), &unhex(message), &r_flipped),
"改 R 必须被拒"
);
let mut s_flipped = array64(&unhex(signature));
s_flipped[63] ^= 0x04;
assert!(
!verify(&array32(&unhex(public)), &unhex(message), &s_flipped),
"改 S 必须被拒"
);
}
#[test]
fn malleable_s_plus_l_is_rejected() {
// 可锻性:把 S 换成 S + L,曲线方程照样成立(S 与 S+L 模 L 同余),严格实现必须拒。
let (public, message, signature) = &rfc_vectors()[0];
let public = array32(&unhex(public));
let message = unhex(message);
// 可锻性:把 S 换成 S + L,曲线方程照样成立(S 与 S+L 模 L 同余)。RFC 8032 的最低要求允许它,
// `verify_strict` 必须拒 —— 这条用例就是「别把 verify_strict 换成 verify」的判据。
let (public, message, signature) = rfc_vectors()[0];
let mut signature = array64(&unhex(signature));
let mut carry = 0_u16;
for index in 0..32 {
@@ -129,21 +145,20 @@ fn malleable_s_plus_l_is_rejected() {
"S + L 不该溢出 256 位(否则这条用例本身没造出来)"
);
assert!(
!verify(&public, &message, &signature),
!verify(&array32(&unhex(public)), &unhex(message), &signature),
"S >= L 必须被拒(可锻变体)"
);
}
#[test]
fn small_order_public_key_is_rejected() {
// 阶为 1 的点(y = 1, x = 0)与阶为 2 的点(y = 0):用它们做公钥,谁都能凑出「自洽」的签名。
// 阶为 1 的点(y = 1, x = 0)与零字节公钥:用它们做公钥,`verify`(非 strict)会接受一批伪造。
let message = b"manifest bytes";
let identity_key = {
let mut key = [0_u8; PUBLIC_KEY_LEN];
key[0] = 1;
key
};
let order_two_key = [0_u8; PUBLIC_KEY_LEN];
let mut signature = [0_u8; SIGNATURE_LEN];
signature[0] = 1; // R = 单位元
assert!(
@@ -151,17 +166,19 @@ fn small_order_public_key_is_rejected() {
"小阶公钥必须被拒"
);
assert!(
!verify(&order_two_key, message, &signature),
!verify(&[0_u8; PUBLIC_KEY_LEN], message, &signature),
"零公钥必须被拒"
);
}
#[test]
fn non_canonical_r_encoding_is_rejected() {
// R 的 y = p(= 2^255-19)与 y = 1 同值但编码不同:非规范写法必须拒。
let (public, message, _) = &rfc_vectors()[0];
let public = array32(&unhex(public));
let message = unhex(message);
// R = p(= 2^255-19)的字节:它与 y = 0 同值但编码不同。
// **dalek 的实际行为(M190 r1 实测)**:`CompressedEdwardsY::decompress` 不单独校验「规范编码」,
// 这个点解压出来是阶为 4 的点 ⇒ 被 `verify_strict` 的**小阶**判据拒。结论(拒)与自写实现的
// 「非规范编码」理由不同,但对这份输入是同一条出路;下面把「公钥非规范编码」也一并钉住,
// 因为那条走的是 `VerifyingKey::from_bytes` 的编码校验。
let (public, message, _) = rfc_vectors()[0];
let p_bytes: [u8; 32] = [
0xed, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
@@ -170,14 +187,23 @@ fn non_canonical_r_encoding_is_rejected() {
let mut signature = [0_u8; SIGNATURE_LEN];
signature[..32].copy_from_slice(&p_bytes);
assert!(
!verify(&public, &message, &signature),
!verify(&array32(&unhex(public)), &unhex(message), &signature),
"非规范 R 编码必须被拒"
);
// 公钥换成同一串非规范编码:`VerifyingKey::from_bytes` 直接拒(连验签都不进)。
assert!(
!verify(
&p_bytes,
&unhex(message),
&array64(&unhex(rfc_vectors()[0].2))
),
"非规范公钥编码必须被拒"
);
}
#[test]
fn empty_signature_is_rejected() {
let (public, message, _) = &rfc_vectors()[0];
let (public, message, _) = rfc_vectors()[0];
assert!(!verify(
&array32(&unhex(public)),
&unhex(message),
@@ -186,28 +212,33 @@ fn empty_signature_is_rejected() {
}
#[test]
fn test_signer_round_trips_and_uses_rfc_seed() {
// 测试钥不是「另写一套算法」:RFC 8032 的 secret 反推出 public 就对上了,签出来的也能验过。
fn signing_key_round_trips() {
// 测试夹具(dalek `SigningKey`)与验签侧同一套语义:RFC 的 secret 反推出的公钥要对得上,
// 签出来的能验过,改一个字节就验不过。
let seed = array32(&unhex(
"9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60",
));
let expected = array32(&unhex(
"d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a",
));
assert_eq!(
public_key_from_seed_for_tests(&seed),
expected,
public_key_hex(&seed),
rfc_vectors()[0].0,
"种子展开必须与 RFC 一致"
);
let signature = sign_for_tests(&seed, b"");
assert_eq!(
signature,
array64(&unhex(&rfc_vectors()[0].2)),
"空消息的签名必须逐字节等于 RFC 向量(证明签名侧与向量同一套算术语义)"
hex_of(&sign_bytes(&seed, b"")),
rfc_vectors()[0].2,
"空消息的签名必须逐字节等于 RFC 向量(测试夹具与向量同一套语义)"
);
let signature = sign_bytes(&seed, b"unlucky");
assert!(verify(
&expected,
&array32(&unhex(rfc_vectors()[0].0)),
b"unlucky",
&sign_for_tests(&seed, b"unlucky")
&signature
));
let mut tampered = signature;
tampered[0] ^= 0x80;
assert!(!verify(
&array32(&unhex(rfc_vectors()[0].0)),
b"unlucky",
&tampered
));
}
+11 -23
View File
@@ -10,13 +10,12 @@ use crate::download::mirrors::SOURCE_GH_PROXY;
use crate::download::{DownloadEngine, RegionProfile, SOURCE_OFFICIAL};
use crate::update::github::{DEFAULT_BASE, DEFAULT_OWNER, DEFAULT_REPO, MANIFEST_ASSET};
use crate::update::{
Channel, DirSource, GitHubReleaseSource, ReleaseManifest, Target, UpdateChecker, UpdateError,
UpdateErrorKind, UpdateSource, Version,
verified_manifest, Channel, DirSource, GitHubReleaseSource, ReleaseManifest, Target,
UpdateChecker, UpdateError, UpdateErrorKind, UpdateSource, Version,
};
use super::support::{
artifact, engine_for, manifest, release, signature_envelope, FakeHttp, FakeResponse, TempDir,
};
use super::support::{artifact, engine_for, manifest, release, FakeHttp, FakeResponse, TempDir};
use super::support_sign::signature_envelope;
const PAYLOAD: &[u8] = b"github release payload";
@@ -130,7 +129,7 @@ fn channels_other_than_stable_publish_nothing() {
source.manifest_url(channel).is_none(),
"{channel} 不该有清单地址"
);
let manifest = source.fetch_manifest(channel).expect("没开通道不是错误");
let manifest = verified_manifest(&source, channel).expect("没开通道不是错误");
assert!(
manifest.is_none(),
"{channel} 应返回 Ok(None)(不发布即无更新)"
@@ -191,8 +190,7 @@ fn fetches_and_parses_the_manifest_from_the_base() {
let server = FakeHttp::start(signed_routes(&stable_manifest_json()));
let source = local_source(&tmp, &server);
let manifest = source
.fetch_manifest(Channel::Stable)
let manifest = verified_manifest(&source, Channel::Stable)
.expect("应取到清单")
.expect("清单存在");
assert_eq!(manifest.channel, Channel::Stable);
@@ -212,9 +210,7 @@ fn missing_manifest_in_the_release_is_not_an_error() {
let server = FakeHttp::start(Vec::new());
let source = local_source(&tmp, &server);
let outcome = source
.fetch_manifest(Channel::Stable)
.expect("404 不是错误");
let outcome = verified_manifest(&source, Channel::Stable).expect("404 不是错误");
assert!(outcome.is_none(), "取不到清单 = 还没发过版");
assert_eq!(
server.hits(),
@@ -230,9 +226,7 @@ fn broken_json_is_reported_as_invalid_manifest() {
let server = FakeHttp::start(signed_routes("{ not json"));
let source = local_source(&tmp, &server);
let error = source
.fetch_manifest(Channel::Stable)
.expect_err("坏 JSON 必须报错");
let error = verified_manifest(&source, Channel::Stable).expect_err("坏 JSON 必须报错");
assert_eq!(error.kind, UpdateErrorKind::InvalidManifest);
assert!(
error.message.contains("github:"),
@@ -250,9 +244,7 @@ fn illegal_manifest_fields_are_reported() {
let server = FakeHttp::start(signed_routes(json));
let source = local_source(&tmp, &server);
let error = source
.fetch_manifest(Channel::Stable)
.expect_err("size 为 0 必须报错");
let error = verified_manifest(&source, Channel::Stable).expect_err("size 为 0 必须报错");
assert_eq!(error.kind, UpdateErrorKind::InvalidManifest);
}
@@ -262,9 +254,7 @@ fn manifest_self_reporting_another_channel_is_a_mismatch() {
let server = FakeHttp::start(signed_routes(&manifest("beta", &[])));
let source = local_source(&tmp, &server);
let error = source
.fetch_manifest(Channel::Stable)
.expect_err("渠道不符必须报错");
let error = verified_manifest(&source, Channel::Stable).expect_err("渠道不符必须报错");
assert_eq!(error.kind, UpdateErrorKind::ChannelMismatch);
}
@@ -274,9 +264,7 @@ fn server_errors_are_network_errors() {
let server = FakeHttp::start(vec![(manifest_path(), FakeResponse::status(500))]);
let source = local_source(&tmp, &server);
let error = source
.fetch_manifest(Channel::Stable)
.expect_err("5xx 必须报错");
let error = verified_manifest(&source, Channel::Stable).expect_err("5xx 必须报错");
assert_eq!(error.kind, UpdateErrorKind::Network);
assert!(error.is_retryable(), "网络抖动值得隔一会儿再试");
}
+1
View File
@@ -30,6 +30,7 @@ mod manifest_cases;
mod signature_cases;
mod source_cases;
mod support;
mod support_sign;
mod target_cases;
mod verify_cases;
mod version_cases;
+19 -28
View File
@@ -17,16 +17,15 @@
use std::fs;
use crate::update::ed25519::public_key_from_seed_for_tests;
use crate::update::{
trust, Channel, DirSource, Job, OsFamily, Target, UpdateChecker, UpdateErrorKind, UpdateSource,
Version,
trust, verified_manifest, Channel, DirSource, Job, OsFamily, Target, UpdateChecker,
UpdateErrorKind, Version,
};
use super::support::{
artifact, hex_of, manifest, release, signature_entry, signature_entry_from_bytes,
signature_envelope, signature_envelope_of, signature_envelope_with, TempDir, TEST_KEY_ID,
TEST_SIGNING_SEED,
use super::support::{artifact, manifest, release, TempDir};
use super::support_sign::{
public_key_hex, sign_bytes, signature_entry, signature_entry_from_bytes, signature_envelope,
signature_envelope_of, signature_envelope_with, TEST_KEY_ID, TEST_SIGNING_SEED,
};
const PAYLOAD: &[u8] = b"signed release payload";
@@ -57,7 +56,7 @@ fn current() -> Version {
fn pinned_test_key_matches_the_test_seed() {
// 信任表里那把测试钥必须就是测试种子推出来的那把。漂了的话「钉住的公钥」与「签名的钥」不是同一个,
// 下面所有正路用例会一起变红——先在这里单独钉死。
let derived = hex_of(&public_key_from_seed_for_tests(TEST_SIGNING_SEED));
let derived = public_key_hex(TEST_SIGNING_SEED);
let pinned = trust::keyring()
.iter()
.find(|key| key.key_id == TEST_KEY_ID)
@@ -92,8 +91,7 @@ fn tampered_field_is_rejected() {
dir.write("stable.json", tampered.as_bytes());
// 签名还是**原始**那份的签名。
dir.write("stable.json.sig", signature_envelope(&original).as_bytes());
let error = DirSource::new(dir.path())
.fetch_manifest(Channel::Stable)
let error = verified_manifest(&DirSource::new(dir.path()), Channel::Stable)
.expect_err("改过字段的清单必须验不过");
assert_eq!(error.kind, UpdateErrorKind::SignatureInvalid);
assert!(
@@ -107,7 +105,7 @@ fn tampered_field_is_rejected() {
fn tampered_signature_only_is_rejected() {
// 守卫①的另一半:清单字节一个不动,只把签名的最后一个字节改掉。
let json = stable_json("0.3.0");
let mut signature = crate::update::ed25519::sign_for_tests(TEST_SIGNING_SEED, json.as_bytes());
let mut signature = sign_bytes(TEST_SIGNING_SEED, json.as_bytes());
let good = signature_envelope(&json);
signature[63] ^= 0x04;
let broken =
@@ -117,8 +115,7 @@ fn tampered_signature_only_is_rejected() {
let dir = TempDir::new("sig-tamper-sig");
dir.write("stable.json", json.as_bytes());
dir.write("stable.json.sig", broken.as_bytes());
let error = DirSource::new(dir.path())
.fetch_manifest(Channel::Stable)
let error = verified_manifest(&DirSource::new(dir.path()), Channel::Stable)
.expect_err("只改签名也必须被拒");
assert_eq!(error.kind, UpdateErrorKind::SignatureInvalid);
}
@@ -127,8 +124,7 @@ fn tampered_signature_only_is_rejected() {
fn missing_signature_is_rejected() {
let dir = TempDir::new("sig-missing");
dir.write("stable.json", stable_json("0.3.0").as_bytes());
let error = DirSource::new(dir.path())
.fetch_manifest(Channel::Stable)
let error = verified_manifest(&DirSource::new(dir.path()), Channel::Stable)
.expect_err("没有签名文件必须拒");
assert_eq!(error.kind, UpdateErrorKind::SignatureMissing);
assert!(!error.is_retryable(), "漏签名不是抖动,重试没有意义");
@@ -140,8 +136,7 @@ fn empty_signature_array_is_malformed() {
let dir = TempDir::new("sig-empty-array");
dir.write("stable.json", stable_json("0.3.0").as_bytes());
dir.write("stable.json.sig", br#"{"format":1,"signatures":[]}"#);
let error = DirSource::new(dir.path())
.fetch_manifest(Channel::Stable)
let error = verified_manifest(&DirSource::new(dir.path()), Channel::Stable)
.expect_err("空签名数组必须拒");
assert_eq!(error.kind, UpdateErrorKind::SignatureMalformed);
}
@@ -156,8 +151,7 @@ fn unknown_field_in_envelope_is_malformed() {
"stable.json.sig",
br#"{"format":1,"signatures":[],"comment":"hi"}"#,
);
let error = DirSource::new(dir.path())
.fetch_manifest(Channel::Stable)
let error = verified_manifest(&DirSource::new(dir.path()), Channel::Stable)
.expect_err("未知字段必须拒");
assert_eq!(error.kind, UpdateErrorKind::SignatureMalformed);
}
@@ -172,8 +166,7 @@ fn unknown_key_is_rejected() {
"stable.json.sig",
signature_envelope_with(ATTACKER_SEED, "attacker-key", &json).as_bytes(),
);
let error = DirSource::new(dir.path())
.fetch_manifest(Channel::Stable)
let error = verified_manifest(&DirSource::new(dir.path()), Channel::Stable)
.expect_err("不认识的钥必须拒");
assert_eq!(error.kind, UpdateErrorKind::SignatureUnknownKey);
assert!(
@@ -194,8 +187,7 @@ fn key_id_spoofing_does_not_help() {
"stable.json.sig",
signature_envelope_with(ATTACKER_SEED, TEST_KEY_ID, &json).as_bytes(),
);
let error = DirSource::new(dir.path())
.fetch_manifest(Channel::Stable)
let error = verified_manifest(&DirSource::new(dir.path()), Channel::Stable)
.expect_err("key_id 撞名不能替代公钥");
assert_eq!(error.kind, UpdateErrorKind::SignatureInvalid);
}
@@ -211,9 +203,8 @@ fn multi_signature_accepts_any_pinned_key() {
signature_entry(TEST_SIGNING_SEED, TEST_KEY_ID, &json),
]);
dir.write("stable.json.sig", merged.as_bytes());
let manifest = DirSource::new(dir.path())
.fetch_manifest(Channel::Stable)
.expect("有一条能过就放行");
let manifest =
verified_manifest(&DirSource::new(dir.path()), Channel::Stable).expect("有一条能过就放行");
assert!(manifest.is_some());
}
@@ -280,7 +271,7 @@ fn no_runtime_keyring_override() {
"stable.json.sig",
signature_envelope_with(ATTACKER_SEED, "attacker-key", &json).as_bytes(),
);
let attacker_public = hex_of(&public_key_from_seed_for_tests(ATTACKER_SEED));
let attacker_public = public_key_hex(ATTACKER_SEED);
dir.write(
"keyring.json",
format!(r#"{{"keys":[{{"key_id":"attacker-key","public_key_hex":"{attacker_public}"}}]}}"#)
@@ -290,7 +281,7 @@ fn no_runtime_keyring_override() {
let keyring_path = dir.join("keyring.json");
std::env::set_var("ULL_UPDATE_KEYRING", &keyring_path);
std::env::set_var("ULL_UPDATE_KEYRING_FILE", &keyring_path);
let outcome = DirSource::new(dir.path()).fetch_manifest(Channel::Stable);
let outcome = verified_manifest(&DirSource::new(dir.path()), Channel::Stable);
std::env::remove_var("ULL_UPDATE_KEYRING");
std::env::remove_var("ULL_UPDATE_KEYRING_FILE");
let error = outcome.expect_err("旁边的信任表文件不许生效");
+5 -10
View File
@@ -3,7 +3,7 @@
use std::fs;
use crate::update::manifest::ReleaseManifest;
use crate::update::{Channel, DirSource, UpdateErrorKind, UpdateSource};
use crate::update::{verified_manifest, Channel, DirSource, UpdateErrorKind, UpdateSource};
use super::support::{artifact, manifest, release, TempDir};
@@ -17,9 +17,7 @@ fn publish(tmp: &TempDir, channel: Channel, json: &str) {
fn missing_channel_manifest_is_not_an_error() {
let tmp = TempDir::new("source-missing");
let source = DirSource::new(tmp.path());
let manifest = source
.fetch_manifest(Channel::Stable)
.expect("缺清单不该报错");
let manifest = verified_manifest(&source, Channel::Stable).expect("缺清单不该报错");
assert!(manifest.is_none(), "还没发过版 = Ok(None)");
}
@@ -36,8 +34,7 @@ fn reads_and_validates_the_channel_manifest() {
);
publish(&tmp, Channel::Stable, &json);
let source = DirSource::new(tmp.path());
let parsed = source
.fetch_manifest(Channel::Stable)
let parsed = verified_manifest(&source, Channel::Stable)
.expect("应取到清单")
.expect("清单存在");
assert_eq!(parsed.channel, Channel::Stable);
@@ -52,8 +49,7 @@ fn channel_mismatch_is_an_error() {
let tmp = TempDir::new("source-mismatch");
// 清单自称 beta,却躺在 stable.json 里
publish(&tmp, Channel::Stable, &manifest("beta", &[]));
let error = DirSource::new(tmp.path())
.fetch_manifest(Channel::Stable)
let error = verified_manifest(&DirSource::new(tmp.path()), Channel::Stable)
.expect_err("渠道不符必须报错");
assert_eq!(error.kind, UpdateErrorKind::ChannelMismatch);
}
@@ -62,8 +58,7 @@ fn channel_mismatch_is_an_error() {
fn broken_manifest_json_is_reported_with_path() {
let tmp = TempDir::new("source-broken");
publish(&tmp, Channel::Stable, "{ not json");
let error = DirSource::new(tmp.path())
.fetch_manifest(Channel::Stable)
let error = verified_manifest(&DirSource::new(tmp.path()), Channel::Stable)
.expect_err("坏 JSON 必须报错");
assert_eq!(error.kind, UpdateErrorKind::InvalidManifest);
assert!(
-66
View File
@@ -37,19 +37,6 @@ impl TempDir {
self.path.join(name)
}
/// 写一份清单**连它的签名**(`<name>` + `<name>.sig`),返回清单路径。
///
/// M190 起「有清单没签名」是被拒的一档,所以夹具一律成对落盘:只写清单的用例都是为了
/// 证明「缺签名会被拒」,它们自己显式 `write` 清单即可。
pub(crate) fn write_signed(&self, name: &str, manifest_text: &str) -> PathBuf {
let path = self.write(name, manifest_text.as_bytes());
self.write(
&format!("{name}.sig"),
signature_envelope(manifest_text).as_bytes(),
);
path
}
/// 写一个文件(父目录自动建),返回路径。
pub(crate) fn write(&self, name: &str, bytes: &[u8]) -> PathBuf {
let path = self.join(name);
@@ -226,59 +213,6 @@ pub(crate) fn manifest(channel: &str, releases: &[String]) -> String {
)
}
/// 测试签名钥的 32 字节种子。
///
/// **它是公开的,故意不保密**:它只对 `cfg(test)` 与 `update-test-source` 构建有意义
/// (`trust::TEST_ONLY` 才认这把钥),正式构建的信任表里没有它,所以拿它签不出任何正式版本
/// 能装的清单。正式私钥只能来自仓库 Secret(见 `packaging/docs/signing.md`)。
pub(crate) const TEST_SIGNING_SEED: &[u8; 32] = b"ull-m190-test-key-seed-000000001";
/// 测试钥的 `key_id`(与 `trust::TEST_ONLY` 里那条一致)。
pub(crate) const TEST_KEY_ID: &str = "test-only-key";
/// 用测试钥签一份清单,返回 `manifest.json.sig` 的信封原文。
pub(crate) fn signature_envelope(manifest_text: &str) -> String {
signature_envelope_with(TEST_SIGNING_SEED, TEST_KEY_ID, manifest_text)
}
/// 用指定种子/`key_id` 签一份清单(负向用例要造「不认识的钥」「改过内容的签名」)。
pub(crate) fn signature_envelope_with(
seed: &[u8; 32],
key_id: &str,
manifest_text: &str,
) -> String {
signature_envelope_of(&[signature_entry(seed, key_id, manifest_text)])
}
/// 多签名信封(轮换窗口的夹具:一份清单同时带几把钥的签名)。
pub(crate) fn signature_envelope_of(entries: &[String]) -> String {
format!(r#"{{"format":1,"signatures":[{}]}}"#, entries.join(","))
}
/// 一条签名条目(可以直接被 [`signature_envelope_of`] 拼起来,也可以单独改坏再拼)。
pub(crate) fn signature_entry(seed: &[u8; 32], key_id: &str, manifest_text: &str) -> String {
let signature = crate::update::ed25519::sign_for_tests(seed, manifest_text.as_bytes());
signature_entry_from_bytes(key_id, &signature, manifest_text)
}
/// 用**现成的签名字节**拼一条条目(「只改签名字节」的用例靠它)。
pub(crate) fn signature_entry_from_bytes(
key_id: &str,
signature: &[u8; 64],
manifest_text: &str,
) -> String {
let sha = crate::update::signature::hex_sha256(manifest_text.as_bytes());
format!(
r#"{{"alg":"ed25519","key_id":"{key_id}","sig":"{}","manifest_sha256":"{sha}"}}"#,
hex_of(signature)
)
}
/// 字节转小写十六进制。
pub(crate) fn hex_of(bytes: &[u8]) -> String {
bytes.iter().map(|byte| format!("{byte:02x}")).collect()
}
/// 造一台测试用下载引擎:镜像表用内置清单,健康度只在内存里,缓存落临时目录。
///
/// `region` 决定「官方优先」还是「镜像优先」——gh-proxy 前缀只在该是后一种时生效。
@@ -0,0 +1,94 @@
//! 清单签名的**测试夹具**:用测试钥签出 `manifest.json.sig` 的信封(M190)。
//!
//! 为什么单独一个文件:`support.rs` 是 update 测试的通用夹具(临时目录、假 HTTP、清单片段),
//! 已经贴着 400 行红线;签名这一摊只被 `signature_cases` / `github_cases` 用到,搬出来两清。
//!
//! 签名走 `ed25519-dalek` 的 `SigningKey`(与仓里验签用的 `verify_strict` 同一棵树),而**不是**
//! 自写实现——M190 r1 复审把自写曲线算术整体删掉了,这里也不留第二条算法。测试钥的种子是公开的
//! (见下),它只对 `cfg(test)` 与 `update-test-source` 构建有意义。
use super::support::TempDir;
/// 测试签名钥的 32 字节种子。
///
/// **它是公开的,故意不保密**:它只对 `cfg(test)` 与 `update-test-source` 构建有意义
/// (`trust::TEST_ONLY` 才认这把钥),正式构建的信任表里没有它,所以拿它签不出任何正式版本
/// 能装的清单。正式私钥只能来自仓库 Secret(见 `packaging/docs/signing.md`)。
pub(crate) const TEST_SIGNING_SEED: &[u8; 32] = b"ull-m190-test-key-seed-000000001";
/// 测试钥的 `key_id`(与 `trust::TEST_ONLY` 里那条一致)。
pub(crate) const TEST_KEY_ID: &str = "test-only-key";
/// 字节转小写十六进制。
pub(crate) fn hex_of(bytes: &[u8]) -> String {
bytes.iter().map(|byte| format!("{byte:02x}")).collect()
}
/// 用测试钥签一份清单,返回 `manifest.json.sig` 的信封原文。
pub(crate) fn signature_envelope(manifest_text: &str) -> String {
signature_envelope_with(TEST_SIGNING_SEED, TEST_KEY_ID, manifest_text)
}
/// 用指定种子/`key_id` 签一份清单(负向用例要造「不认识的钥」「改过内容的签名」)。
pub(crate) fn signature_envelope_with(
seed: &[u8; 32],
key_id: &str,
manifest_text: &str,
) -> String {
signature_envelope_of(&[signature_entry(seed, key_id, manifest_text)])
}
/// 多签名信封(轮换窗口的夹具:一份清单同时带几把钥的签名)。
pub(crate) fn signature_envelope_of(entries: &[String]) -> String {
format!(r#"{{"format":1,"signatures":[{}]}}"#, entries.join(","))
}
/// 一条签名条目(可以被 [`signature_envelope_of`] 拼起来,也可以单独改坏再拼)。
pub(crate) fn signature_entry(seed: &[u8; 32], key_id: &str, manifest_text: &str) -> String {
signature_entry_from_bytes(
key_id,
&sign_bytes(seed, manifest_text.as_bytes()),
manifest_text,
)
}
/// 用**现成的签名字节**拼一条条目(「只改签名字节」的用例靠它)。
pub(crate) fn signature_entry_from_bytes(
key_id: &str,
signature: &[u8; 64],
manifest_text: &str,
) -> String {
let sha = crate::update::signature::hex_sha256(manifest_text.as_bytes());
format!(
r#"{{"alg":"ed25519","key_id":"{key_id}","sig":"{}","manifest_sha256":"{sha}"}}"#,
hex_of(signature)
)
}
/// 用 32 字节种子签一段字节(dalek 的 `SigningKey`;测试专用,正式构建里没有签名代码)。
pub(crate) fn sign_bytes(seed: &[u8; 32], message: &[u8]) -> [u8; 64] {
use ed25519_dalek::{Signer, SigningKey};
let key = SigningKey::from_bytes(seed);
key.sign(message).to_bytes()
}
/// 种子对应的公钥(用来让「钉住的公钥」与「签名的钥」对得上——漂了就当场红)。
pub(crate) fn public_key_hex(seed: &[u8; 32]) -> String {
use ed25519_dalek::SigningKey;
hex_of(SigningKey::from_bytes(seed).verifying_key().as_bytes())
}
impl TempDir {
/// 写一份清单**连它的签名**(`<name>` + `<name>.sig`),返回清单路径。
///
/// M190 起「有清单没签名」是被拒的一档,所以夹具一律成对落盘:只写清单的用例都是为了
/// 证明「缺签名会被拒」,它们自己显式 `write` 清单即可。
pub(crate) fn write_signed(&self, name: &str, manifest_text: &str) -> std::path::PathBuf {
let path = self.write(name, manifest_text.as_bytes());
self.write(
&format!("{name}.sig"),
signature_envelope(manifest_text).as_bytes(),
);
path
}
}