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
Generated
+2
-2
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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 拒"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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. 本地自测(不需要私钥)
|
||||
|
||||
|
||||
@@ -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 的同一组向量把两边各自钉住。
|
||||
|
||||
Reference in new issue
Block a user