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:
1 parent
918d826645
commit
a219a21db8
5 files changed
+83
-41
No files matched your search
@@ -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))
|
||||
|
||||
@@ -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 拒"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user