fix(m190): r2 复审——dalek strict 口径照实测写正、补小阶 R 用例、锁里 toml_datetime 退回 1.1.2

三条 P2 都是「文档/注释与事实不符 + 一条上报事实错了」,代码行为没变。

P2-1 dalek 的 strict vs 非 strict 口径改成正测试出来的样子:
* `VerifyingKey::from_bytes` **不**校验编码规范性(`y = p` 的非规范写法返回 Ok,`is_weak() == true`),
  先前写的「要求规范编码」是错的;
* 非 strict 的 `verify` **也**查 `S` 规范性 —— 先前把它写成「不做可锻检查」是半错;
* 于是这批拒绝类用例里,strict 与非 strict 唯一的分歧是**小阶点**:承重的是
  `small_order_public_key_is_rejected`,其余几条(malleable / non_canonical_r / empty)换掉 strict 后
  **仍然绿**,文件头的表与用例注释都改成这个口径,别让人误以为它们也是守卫。
* 新补 `small_order_r_is_rejected_only_by_strict`:R = 单位元 + S = k·a mod L 的伪造,**两边都断言**
  —— dalek 的非 strict `verify` 接受它、我们的 `verify` 拒绝。这是第二条「只由 strict 拒」的判据。
* `non_canonical_r_encoding_is_rejected` 里那条「非规范公钥编码」原来对两种实现都绿(等于没测),
  现在断言 dalek 的**真实**行为(`from_bytes(...).is_ok()` + `is_weak()`),dalek 升级改口径会红。

P2-2 `packaging/sign_manifest.py` 里指向已删除 `ed25519/{field,group}.rs` 的旧注释删掉(那段还写着
「same formulas」,而两套实现现在是**互相独立**才构成交叉验证)。

P2-3 `Cargo.lock`:`toml_datetime` 从 1.1.3 **退回 1.1.2**(`cargo update -p toml_datetime --precise
1.1.2` 实测可降、`cargo metadata --locked` 自洽)。先前报告里「被 `toml` 拒绝所以无法避免」的理由
**不成立**,已在报告里更正。现在相对 `44a0c87` 的锁变更是**纯新增**:16 个包、零既有版本变动。

变异证明(本轮按要求重跑):把 `verify_strict` 换成 `verify`(并补 `Verifier` trait 使其可编译)
⇒ `test result: FAILED. 110 passed; 2 failed`,红的两条正是
`small_order_public_key_is_rejected` 与 `small_order_r_is_rejected_only_by_strict`;其余拒绝类用例全绿
(与修正后的口径一致)。恢复后 `112 passed; 0 failed`。
This commit is contained in:
AskaEth committed 2026-11-09 02:33:08 +08:00
1 parent 918d826645
commit a219a21db8
5 files changed
+83 -41

No files matched your search

Generated
+2 -2
View File
@@ -4881,9 +4881,9 @@ dependencies = [
[[package]]
name = "toml_datetime"
version = "1.1.3+spec-1.1.0"
version = "1.1.2+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "173f25c883cd2176a2d8212155638c0a60d484a9d84546d270866f40f56b6e49"
checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053"
dependencies = [
"serde_core",
]
+15 -13
View File
@@ -4,21 +4,23 @@
//!
//! M190 第一版为了「scope 里不含 `crates/core/Cargo.toml`」把曲线算术写在了仓里
//! (5x51 素域 + 扩展坐标 + 平方-乘)。r1 复审的裁定是**不允许**:这是**信任锚**,
//! 非规范编码、小阶点、批量验证差异这类微妙错误的后果是「验签通过但本不该通过」,
//! 自写的向量覆盖再全也证明不了「严格性判据选对了」。当时的时机也最好:正式信任表
//! (`trust::PRODUCTION`)还是空表,换实现**零兼容负担**。
//! 非规范编码、小阶点这类微妙错误的后果是「验签通过但本不该通过」,自写的向量覆盖再全也证明不了
//! 「严格性判据选对了」。当时的时机也最好:正式信任表(`trust::PRODUCTION`)还是空表,换实现
//! **零兼容负担**。于是这里只剩一层薄封装。
//!
//! 于是这里只剩一层薄封装,判据全部交给 dalek:
//! # 判据分工(**实测口径**,`ed25519-dalek 2.2.0`;别照抄「常识」)
//!
//! * `VerifyingKey::from_bytes` —— 公钥必须是**规范编码的合法点**(非规范 `y` / 不在曲线上都拒);
//! * `VerifyingKey::verify_strict` —— 比 RFC 8032 的最低要求更严:拒绝**小阶**公钥与小阶 `R`
//! (阻止「谁都能构造自洽四元组」的伪造族),并拒绝**非规范标量 `S`**(`S >= L` 的可锻变体);
//! * `Signature::from_bytes` —— 只搬运 64 字节;真正的判据在 `verify_strict` 里,**不能**换成
//! `verify`(后者不做小阶/可锻检查)。
//! | 步骤 | 它实际做什么 | 它**不**做什么 |
//! |---|---|---|
//! | `VerifyingKey::from_bytes` | 解压成曲线上的合法点;解不出来才 `Err` | **不**校验编码规范性:`y >= p` 这种第二种写法照样 `Ok`(此时 `is_weak() == true`)。M190 r2 复审实测并纠正了先前「要求规范编码」的说法 |
//! | `VerifyingKey::verify`(**非 strict**,本仓不用) | 验方程 `[S]B = R + [k]A`,并检查 `S` 是**规范标量**(`S >= L` 会拒) | **不**检查小阶点:小阶公钥 / 小阶 `R` 下「自洽的伪造四元组」能验过 |
//! | `VerifyingKey::verify_strict`(**本仓用的这个**) | 上面全部 + 拒**小阶公钥**与**小阶 `R`** | —— |
//!
//! 这批行为由 `tests/ed25519_cases.rs` 逐条钉住(RFC 8032 四条向量 + 六条「该拒就拒」):
//! 那些用例原来用来测自写实现,现在改成**钉 dalek 的 strict 语义**——将来谁把
//! `verify_strict` 换成 `verify`(或换回自写实现),它们会当场红。
//! 换成大白话:在这批拒绝类用例里,strict 与**非** strict 唯一的分歧是**小阶点**(不是可锻性——
//! `verify` 自己就查 `S` 规范性)。所以守卫里真正承重的是
//! `tests/ed25519_cases.rs::small_order_public_key_is_rejected` 与
//! `small_order_r_is_rejected_only_by_strict` 这两条;`malleable_s_plus_l_is_rejected` 等其余几条在
//! 非 strict 下**照样绿**,它们钉的是「判据没被删成恒真」,不要误当成 strict 的守卫。
//!
//! # 签名侧不在二进制里
//!
@@ -42,7 +44,7 @@ pub(crate) fn verify(
signature: &[u8; SIGNATURE_LEN],
) -> bool {
let Ok(key) = VerifyingKey::from_bytes(public_key) else {
// 公钥都不是规范编码的合法点:直接拒(`from_bytes` 已经做了曲线方程与规范编码检查)。
// 不是曲线上的合法点:直接拒。注意 `from_bytes` **不**管编码规范性(见模块文档的表)。
return false;
};
key.verify_strict(message, &Signature::from_bytes(signature))
+62 -20
View File
@@ -3,17 +3,21 @@
//! 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`)与验签侧是同一套语义 |
//! | 用例 | 它挡住什么 | 把 `verify_strict` 换成 `verify` 后会怎样(r2 实测) |
//! |---|---|---|
//! | `small_order_public_key_is_rejected` | 小阶公钥下「自洽的伪造」(谁都能凑) | **红** —— 这是**唯一**承重的守卫 |
//! | `small_order_r_is_rejected_only_by_strict` | 小阶 `R`(R = 单位元)的同一族伪造 | **红**(其余几条不会) |
//! | `rfc8032_vectors_verify` | 实现换成任何一个错的版本(向量来自 RFC 附录) | 仍绿(`verify` 也验方程) |
//! | `malleable_s_plus_l_is_rejected` | `S >= L` 的可锻变体 | **仍绿**:`verify` 自己就查 `S` 规范性,别把它当 strict 的守卫 |
//! | `non_canonical_r_encoding_is_rejected` | 非规范编码的点(`y >= p` 的第二种写法) | 仍绿(dalek 靠自己的下游判据拒,见该用例注释) |
//! | `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 拒它的理由是**小阶**,不是「非规范编码」单独一条)。
//! 口径来源:M190 r2 复审在 `ed25519-dalek 2.2.0` 上逐条实测(并把先前的四处错误说法打回)。
//! **结论**:strict 与**非** strict 在这批输入上唯一的分歧是**小阶点**——所以想确认「strict 还开着」,
//! 看前两条;其余几条只在「判据被删成恒真」时才会红。
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
use crate::update::ed25519::{verify, PUBLIC_KEY_LEN, SIGNATURE_LEN};
@@ -130,8 +134,9 @@ fn signature_bit_flip_is_rejected() {
#[test]
fn malleable_s_plus_l_is_rejected() {
// 可锻性:把 S 换成 S + L,曲线方程照样成立(S 与 S+L 模 L 同余)。RFC 8032 的最低要求允许它,
// `verify_strict` 必须拒 —— 这条用例就是「别把 verify_strict 换成 verify」的判据。
// 可锻性:把 S 换成 S + L,曲线方程照样成立(S 与 S+L 模 L 同余)。RFC 8032 的最低要求允许它。
// ⚠ 这条**不是** strict 的守卫:dalek 的非 strict `verify` 自己就检查 `S` 规范性,换掉 strict 它
// 仍绿(r2 实测)。它挡的是「判据被删成恒真」。strict 的守卫见 `small_order_*` 两条。
let (public, message, signature) = rfc_vectors()[0];
let mut signature = array64(&unhex(signature));
let mut carry = 0_u16;
@@ -173,11 +178,13 @@ fn small_order_public_key_is_rejected() {
#[test]
fn non_canonical_r_encoding_is_rejected() {
// R = p(= 2^255-19)的字节:它与 y = 0 同值但编码不同。
// **dalek 的实际行为(M190 r1 实测)**:`CompressedEdwardsY::decompress` 不单独校验「规范编码」,
// 这个点解压出来是阶为 4 的点 ⇒ 被 `verify_strict` 的**小阶**判据拒。结论(拒)与自写实现的
// 「非规范编码」理由不同,但对这份输入是同一条出路;下面把「公钥非规范编码」也一并钉住,
// 因为那条走的是 `VerifyingKey::from_bytes` 的编码校验。
// R = p(= 2^255-19)的字节:它与 y = 0 同值但编码不同(非规范)。
// dalek 的实际行为(r2 实测,与先前的说法不同):
// * `CompressedEdwardsY::decompress` **不**校验编码规范性 —— 这串字节解出来是**阶为 4**的点;
// * 于是它被 `verify_strict` 的**小阶**判据拒,而不是被「编码非规范」拒;
// * 公钥侧同理:`VerifyingKey::from_bytes` 对非规范编码返回 **Ok**(`is_weak() == true`),
// 所以要拒它只能靠下游判据 —— 下面把 dalek 的这个真实行为**断言下来**,将来 dalek 升级
// 改了口径会红(否则这条断言对任何实现都绿,等于没测)。
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,
@@ -188,16 +195,51 @@ fn non_canonical_r_encoding_is_rejected() {
signature[..32].copy_from_slice(&p_bytes);
assert!(
!verify(&array32(&unhex(public)), &unhex(message), &signature),
"非规范 R 编码必须被拒"
"非规范 R 编码必须被拒(dalek 的理由是小阶点)"
);
// 公钥换成同一串非规范编码:`VerifyingKey::from_bytes` 直接拒(连验签都不进)。
// dalek 的真实行为:非规范编码的公钥**能**通过 `from_bytes`,并被标成 weak。
let weak = VerifyingKey::from_bytes(&p_bytes).expect("dalek 2.2 不校验编码规范性,应返回 Ok");
assert!(
weak.is_weak(),
"这个非规范编码解出的是小阶点,dalek 应标记 is_weak"
);
// 我们的 `verify` 对同一串公钥仍然拒(下游方程/小阶判据兜住)。
assert!(
!verify(
&p_bytes,
&unhex(message),
&array64(&unhex(rfc_vectors()[0].2))
),
"非规范公钥编码必须被拒"
"非规范/小阶公钥不得验过"
);
}
#[test]
fn small_order_r_is_rejected_only_by_strict() {
// **第二条只由 strict 承重的判据**(r2 复审建议补的):小阶 `R`。
// 构造:R = 单位元(阶 1),S = k·a mod L,其中 k = SHA-512(R‖A‖M) mod L、a 是测试钥的标量。
// 这样方程 `[S]B = R + [k]A` 自洽 ⇒ **非 strict 的 `verify` 会通过**;`verify_strict` 因
// 「R 是小阶点」拒。下面两边都断言,所以将来谁把 strict 换成非 strict,这条会红。
let public = array32(&unhex(
"f3ea49a34ebda73d6500c9416f425e0358baf094b957baf3ea7daa6df63e876b",
));
let message = b"manifest bytes";
let forged = array64(&unhex(concat!(
"0100000000000000000000000000000000000000000000000000000000000000",
"698a94a4c82aa13c1a4da6248799849f2254ef333b5f5d69dcbd71b6320e5e0e"
)));
// 非 strict 侧:dalek 的 `verify` 会接受这条伪造(这就是 strict 存在的理由)。
let key = VerifyingKey::from_bytes(&public).expect("合法公钥");
assert!(
key.verify(message, &Signature::from_bytes(&forged)).is_ok(),
"非 strict 的 verify 应当接受这条伪造(构造前提;若 dalek 改了行为,这条先红)"
);
// strict 侧:我们导出的 `verify` 必须拒。
assert!(
!verify(&public, message, &forged),
"小阶 R 的伪造必须被 verify_strict 拒"
);
}
+4 -2
View File
@@ -131,13 +131,15 @@ python3 packaging/sign_manifest.py --verify-pinned dist/manifest.json # ④ 用
| 哪一半 | 在哪 | 用什么 |
|---|---|---|
| **验签**(进二进制) | `crates/core/src/update/ed25519.rs` | `ed25519-dalek::verify_strict`(拒非规范编码 / 小阶点 / 非规范 `S`) |
| **验签**(进二进制) | `crates/core/src/update/ed25519.rs` | `ed25519-dalek::verify_strict`(在非 strict 的基础上额外拒**小阶公钥**与**小阶 `R`**;`S` 规范性是非 strict 的 `verify` 也查的) |
| **签名**(只在发布侧) | `packaging/ed25519_ref.py` + `packaging/sign_manifest.py` | 纯 Python 参考实现,零第三方依赖(CI 不装 pip 包) |
两套实现互相独立,`--self-test` 用 RFC 8032 §7.1 的同一组向量把两边各自钉住。**第一版曾把验签写成
仓内自写**(5x51 素域 + 扩展坐标);r1 复审裁定不安全——信任锚的微妙错误后果是「验签通过但本不该通过」,
因此整段删除,改为官方实现。`crates/core/src/update/tests/ed25519_cases.rs` 现在钉的是
**dalek 的 strict 语义**:谁把 `verify_strict` 换成 `verify`(不做小阶/可锻检查),那批用例当场红。
**dalek 的 strict 语义**:谁把 `verify_strict` 换成 `verify`,`small_order_public_key_is_rejected` 与
`small_order_r_is_rejected_only_by_strict` 会红(这是 r2 实测的结论——其余几条拒绝类用例在非 strict
下仍绿,别把它们当 strict 的守卫)。
## 8. 本地自测(不需要私钥)
-4
View File
@@ -36,10 +36,6 @@ TRUST_RS = Path(__file__).resolve().parent.parent / "crates/core/src/update/trus
PIN_BEGIN = ">>> PINNED-KEYS >>>"
PIN_END = "<<< PINNED-KEYS <<<"
KID_RE = re.compile(r'key_id:\s*"([^"]+)"\s*,\s*public_key_hex:\s*"([0-9a-fA-F]{64})"')
# Ed25519 (RFC 8032), reference-shaped implementation (same formulas as
# `crates/core/src/update/ed25519/*`). Validated against the RFC 8032 §7.1 vectors in
# `--self-test`; the Rust verifier is validated against the *same* vectors, so the two
# implementations cross-check each other. No third-party dependency: CI needs no pip install.
# Ed25519 的参考实现搬到了 `packaging/ed25519_ref.py`(M190 r1:本文件贴着 400 行红线)。
# 目的与路径都写在那边的文件头:发布侧签名必须零第三方依赖,而仓里验签用的是 ed25519-dalek ——
# 两套实现互相独立,`--self-test` 用 RFC 8032 的同一组向量把两边各自钉住。