Skip to content

fix(hid-acceptance): 注入器用稳定签名身份 + 源 sha256 戳,修人工 TCC 授权跨运行失效(#58) - #59

Closed
gqf2008 wants to merge 4 commits into
mainfrom
fix/hid-harness-58
Closed

gqf2008 wants to merge 4 commits into
mainfrom
fix/hid-harness-58

Conversation

@gqf2008

@gqf2008 gqf2008 commented Sep 16, 2026 •

Copy link
Copy Markdown
Owner

Summary

修 issue #58:HID 验收脚本的注入器 app 每轮被重写二进制 → 人工 TCC 授权跨运行失效(D 段"授权一次、之后都应可用"只在授权当次为真)。

根因(用 TCC 库里的 csreq 钉死,不是推测)

com.vdev.hid.injector:  csreq=40B   →  cdhash H"e7c71dc6a91a25c067943fd46bddb0774854efae"
com.vdev.camera.host :  csreq=160B  →  identifier "com.vdev.camera.host" and anchor apple generic …

未签名的 app,TCC 存的是纯 cdhash 要求;脚本每轮 cp 新的 vdev 二进制进 bundle → cdhash 变 → 授权失配。已签名的 VDCamera 则是身份型要求,不受二进制更新影响。

Changes

  • 稳定签名:codesign --force --sign "<Apple Development: …(7L8FV63FAP)>" -i com.vdev.hid.injector --options runtime --timestamp=none。签名后 designated requirement = identifier "com.vdev.hid.injector" and anchor apple generic and certificate leaf[subject.CN] = "Apple Development: qingfeng gao (7L8FV63FAP)" …,不含 cdhash。
  • 只在源二进制变了才重写+重签:用 ~/.vdev-hid-injector/.injector-source.sha256 记源 sha256。注意不能用 cmp 比较 bundle 里的可执行文件——codesign 把签名嵌进 Mach-O,签过名的文件永远不等于源文件(第一版就是这么写错的,实测两次连跑都在重签)。
  • 签名失败时明确 WARN(那种情况下 D 段无法持久),并提示:系统设置里若有旧条目,那是 cdhash 型的失效授权,要先 - 删掉再重新添加。
  • 脚本把 designated requirement 打出来做证据,出现 cdhash 字样即告警。
  • 顺带修一个把 B 段偶发打红的杂音:探针在写 READY 之前清零计数(开窗瞬间常有上一个用例的迟到合成事件落进来,实测偶发一个前导空格,导致 ' authed-identity-ok' != 'authed-identity-ok');harness 再在 READY 后加 0.3s 静默期。
  • macos-smoke.sh 补 4 条守卫:必须稳定签名 / 必须用 sha256 戳 / 探针必须清零 —— 删掉任一条修复,smoke 就会红。

Verification

签名身份稳定性(不需要人工授权即可验证)

步骤 结果
签名后 codesign -dr - identifier "com.vdev.hid.injector" and anchor apple generic and certificate leaf[subject.CN] = "Apple Development: qingfeng gao (7L8FV63FAP)" and certificate 1[field.1.2.840.113635.100.6.2.1] exists
把 bundle 内可执行文件从 vdev 换成 /bin/echo 后重签 DR 一字不变(CDHash 变),即"二进制更新不动授权依据"

不再无谓重写

运行 重签次数 结果
第 1 次 1(源变了) PASS
第 2、3 次 0 PASS(A/A2/B/C 三条逐条一致)

D 段跨运行持久(已补,见下方收口评论):人工授权 ~/.vdev-hid-injector/VdevHidInjector.app 后连跑 3 次,D 段每次 post_access=true + 逐字送达 22 字符;TCC 里的授权要求已从 40B cdhash H"e7c71dc6…" 变为 164B 身份型(identifier + anchor apple generic + certificate leaf[subject.CN])。

本地门禁:fmt / check / test(151)/ clippy -D warnings / build --release / macos-smoke / check-docs 全绿。

Model Used

Codex(OpenAI coding agent),macOS 26.5.2 Apple Silicon 真机。

Issue

Fixes #58
Refs #56

issue #58:D 段("人工授权一次 → 之后都应可用")只在授权当次为真。根因用 TCC 库里的
csreq 钉死了——未签名 app 的授权要求是 **纯 cdhash**:

    com.vdev.hid.injector: csreq=40B  →  cdhash H"e7c71dc6a91a25c067943fd46bddb0774854efae"

而脚本每轮都把新的 vdev 二进制拷进 bundle,cdhash 一变授权立刻失配
(对照:签名版 VDCamera 的 csreq 是 identifier + anchor apple generic 的身份型要求)。

改法:
- `codesign --force --sign "<Apple Development/Developer ID>" -i com.vdev.hid.injector
  --options runtime --timestamp=none`:签名后 designated requirement 变成
  `identifier "com.vdev.hid.injector" and anchor apple generic and certificate
  leaf[subject.CN] = "Apple Development: qingfeng gao (7L8FV63FAP)" and ...`,与二进制无关。
  实测把可执行文件从 vdev 换成 /bin/echo 重签,DR **一字不变**(CDHash 变)。
- 只在源二进制变了才重写+重签:用 `~/.vdev-hid-injector/.injector-source.sha256`
  记录源 sha256。**不能**用 `cmp` 比较 bundle 内二进制——codesign 会把签名嵌进
  Mach-O,签过名的可执行文件永远不等于源文件(第一版就是这么写错的)。
- 签名不可用时明确 WARN:那种情况下 D 段无法跨运行复现(并提示列表里的旧条目是
  cdhash 型失效授权,要先删掉再重加)。
- 脚本把 designated requirement 打出来,出现 `cdhash` 字样就告警。

顺带修一个把 B 段偶发打红的杂音:探针在写 READY 之前清零计数(开窗瞬间常有上一个
用例的迟到合成事件落进来,实测偶发一个前导空格);harness 在 READY 后加 0.3s 静默期。

macos-smoke.sh 补 4 条守卫(必须稳定签名、必须用 sha256 戳、探针必须清零),
删掉任一条修复都会红。

实测:连续 3 次运行全部 PASS、`重签=0`(不再无谓重写),A/A2/B/C 三次逐条一致。
D 段的"授权后跨运行持久"需要一次人工授权,见 PR 说明。

Refs #58
复审给了 3 条阻塞项,本提交收前两条(第三条 B3 是证据,已用真机三次连跑补齐,见 PR)。

B1 跳过决策只看源 sha,会把"签名坏了 / 身份降级了"永久固化:
- 注入器准备抽成 `prepare_injector_app <src> <app> <stamp> <identity>`,**复用条件
  三者齐备**:源 sha 同名 + 期望身份一致 + `codesign --verify --strict` 通过。
- 戳记状态而不只是 sha(`<sha256>\t<signed:身份|adhoc>`),身份恢复后能自动重签。
- **只有签名 + 校验都成功才写戳**:签名失败/校验失败一律不写,下次重试(此前
  `else` 分支也写戳,会把"尝试过"记成"准备好了"——正是 #58 的失效模式在修复自身复现)。
- 身份解析改 `resolve_sign_identity()`:Apple Development 优先,Developer ID 兜底,
  都没有才 ad-hoc 并 WARN。
- `Info.plist` 只在内容变了才写(无条件重写会让签名失效)。
- 加互斥锁(`~/.vdev-hid-injector/.lock`):实测 6 路并发会把 bundle 签成
  `invalid Info.plist (plist or signature have been modified)`;现在并发直接 NOT_RUN。
- 打印 `codesign --verify --strict` 结果(此前只看 DR 里有没有 cdhash,看不出"签名已坏")。

B2 纯 grep 守卫挡不住"语义摘除":
- 新增 `--self-test`(隔离临时目录,不需要证书/前台焦点):① 首跑写状态戳
  ② 源与身份都没变 → **不重写**(mtime/inode 不变)③ 源换了 → 重签且戳更新
  ④ bundle 被改坏 → 戳匹配也不复用、自动重做 ⑤ 身份变化 → 重签。
- `macos-smoke.sh` 直接跑这个自测,并补精确字符串守卫(调用点必须传
  `"${INJECTOR_SIGN_IDENTITY}"`、必须 `security find-identity` 解析、必须有
  `codesign --verify --strict`)。
- 阳性对照实测:① 身份传 `-` → smoke 红(字符串守卫)② 删掉写戳 → smoke 红
  (自测报"首跑没有写戳")——后者正是旧守卫抓不到的那一类。

顺带(与 #58 无关但同属本 harness 稳定性,单列在此):
- 探针在写 READY 前清零 typed/keys/tapCount,排除开窗瞬间的迟到合成事件
  (此前偶发一个前导空格把 B 段"逐字一致"打红);注释同步改成"清零即基线"。
- READY 后的 0.3s 静默期对 app/direct 两种注入方式都生效(此前只覆盖 direct)。
- D 段未授权时记 `env_bad`(整体走 NOT_RUN/exit 2):"没验证 ≠ 通过",
  否则未授权也会输出 PASS,把 #58 的验收读成已完成。

真机:注入器授权后 **3 次连跑 D 段均 `post_access=true` + 逐字送达 22 字符**,
全程 `fails=0 env_bad=0`;TCC 里该身份的授权要求已从 40B `cdhash H"e7c71dc6…"`
变成 164B 身份型(`identifier "com.vdev.hid.injector" and anchor apple generic and
certificate leaf[subject.CN] = "Apple Development: qingfeng gao (7L8FV63FAP)"`)。

Refs #58
@gqf2008

gqf2008 commented Sep 16, 2026

Copy link
Copy Markdown
Owner Author

B1/B2 收口 + B3 证据(commit 80ae5f1)

B1(跳过决策会把坏状态固化)已闭

  • 准备逻辑抽成 prepare_injector_app <src> <app> <stamp> <identity>,复用条件三者齐备:源 sha 同名 且 期望身份一致 且 codesign --verify --strict 通过。
  • 戳记状态(<sha>\t<signed:身份|adhoc>)而非只记 sha → 身份恢复后会自动重签。
  • 只有签名 + 校验都成功才写戳(此前 else 分支也写戳,把"尝试过"记成"准备好了"——正是 #58 的失效模式在修复自身复现,审查抓得很准)。
  • Info.plist 只在内容变了才写(无条件重写会让签名失效);加互斥锁,并发直接 NOT_RUN(实测 6 路并发能把 bundle 签成 invalid Info.plist)。
  • 打印 codesign --verify --strict 结果 —— 此前只看 DR 里有没有 cdhash,看不出"签名已坏"。

B2(纯 grep 守卫挡不住语义摘除)已闭

新增 --self-test(隔离临时目录、不需要证书与前台焦点):① 首跑写状态戳 ② 源与身份都没变 → 不重写(mtime/inode 不变)③ 源换了 → 重签且戳更新 ④ bundle 被改坏 → 戳匹配也不复用、自动重做 ⑤ 身份变化 → 重签。macos-smoke.sh 直接跑它,并补精确字符串守卫。

阳性对照(我自己喂的):

变异(就是你列的语义写法) smoke
身份传 -(退回 ad-hoc,正是 #58 本身) 红(字符串守卫)
删掉写戳那行 红(自测报 FAIL: 首跑没有写戳)
删掉复用判断 红(自测第 2 步报"源没变却重写了")

B3(#58 的核心验收:授权跨运行持久)证据已补

人工授权 ~/.vdev-hid-injector/VdevHidInjector.app(旧条目已删)后:

TCC 里的授权要求变了(机制层面的证据):

修复前: csreq=40B   → cdhash H"e7c71dc6a91a25c067943fd46bddb0774854efae"
修复后: csreq=164B  → identifier "com.vdev.hid.injector" and anchor apple generic and
                       certificate leaf[subject.CN] = "Apple Development: qingfeng gao (7L8FV63FAP)" and ...

三次连跑(每次都会走一遍准备逻辑):

第 1 次: post_access=true  inject_out=typed 22 chars  PASS D  fails=0 env_bad=0
第 2 次: post_access=true  inject_out=typed 22 chars  PASS D  fails=0 env_bad=0
第 3 次: post_access=true  inject_out=typed 22 chars  PASS D  fails=0 env_bad=0

并且重签之后授权依然有效:收口提交改动了准备逻辑,脚本自动重签了一次(codesign --verify --strict: OK、DR 仍是身份型),随后的真实运行 D 段依旧 post_access=true + 逐字送达 —— 这正是 #58 要验证的"二进制更新不动授权依据"。

顺手(S 系列)

D 段未授权时改记 env_bad(整体 NOT_RUN/exit 2,"没验证 ≠ 通过");0.3s 静默期对 app/direct 两种注入方式都生效;探针注释改成"清零即基线"。

本地门禁:fmt / check / test(151)/ clippy -D warnings / build --release / macos-smoke(含新自测)/ check-docs 全绿。

复审第二轮抓到两条:

L1 互斥锁形同虚设:`cleanup` 无条件 `rmdir` 锁目录,于是"拿到 NOT_RUN 的人重跑"
会把正在跑的实例的锁删掉,第三个实例就能与第一个并发改同一个 bundle(实测 6 路
并发能把 bundle 签成 `invalid Info.plist`);`--self-test` 同样会误删活锁。
改为 `LOCK_HELD` 标记,**只有持锁者**在 EXIT 里删锁;NOT_RUN 文案给出人工清锁命令。

L2 戳会撒谎:`want_state` 由**请求的**身份推导,而 `--sign -`(或证书不可用)时
codesign 仍会以 ad-hoc 成功 → 戳写成 `signed:<身份>`、复用条件把降级的 bundle
当"最新",永不自愈(这正是 #58 的失效模式换了个地方复现)。改为签完用实际结果
推导状态(`codesign -dv` 的 `Signature=adhoc` / `Authority=`):请求了稳定身份却
落成 ad-hoc 时**不写戳**并返回 2(下轮重试),写进戳的永远是盘上真实状态。
自测补第 6 步:用不可用身份签名后,戳里不得出现 `signed:<该身份>`。
smoke 再补一条字符串守卫钉住函数内必须 `--sign "${identity}"`(写成 `-` 会红)。

阳性对照(改完即验):
- 有活跃锁时跑 `--self-test` → 锁存活 ✓;锁被占时整脚本 NOT_RUN 且退出后锁仍在 ✓
- 函数内签名改成 `--sign -` → smoke 红 ✓
- 删掉写戳 / 身份传 `-` → smoke 红 ✓(沿用上一轮的两条对照)

顺带:删掉未使用的 `SELF_SHA1`(shellcheck SC2034);签名失败时不再误报
"已用稳定身份签名"(按 prepare 的返回码 0/1/2 分别输出)。

真机:注入器准备逻辑重签一次后,连续两次整脚本 `fails=0 env_bad=0`、D 段
`post_access=true` 逐字送达;戳内容为
`<sha256>\tsigned:Apple Development: qingfeng gao (7L8FV63FAP)`(实际签名状态)。

Refs #58
复审第三轮通过(无阻塞项),顺手收掉它列的两条非阻塞项 + 一条说明:

- 失败路径返回 2:`codesign` 直接失败 / `verify --strict` 失败此前都 `return 0`,
  调用点会把"没签成"打成"已用稳定身份签名:<身份>"(与紧邻的 `verify: FAILED` 同屏
  自相矛盾)。现在返回 2 走"本次没签成稳定身份,下轮重试"分支。

- 戳记三段 `<源 sha>\t<请求身份>\t<实际签名状态>`,复用条件同时核三段 + verify:
  * 只比 sha → 坏签名/身份降级会被固化(上一轮 B1);
  * 只比"实际状态" → 传证书别名(如 SHA-1)时,请求串与 `Authority=` 叶子 CN 不等,
    会变成"每轮都重签"(复审实测 inode 每轮都变),丢掉 #58 想保住的"必要时才重写";
  * 三段分开存后:显式换身份 → 重签 ✓;别名写法 → 复用 ✓;盘上签名与戳不符 → 重做 ✓。
  抽 `signature_state()` 统一"当前实际签名状态"口径(复用判定与写戳共用)。

- 脚本头补一条事实:真机注入的"逐字一致"断言在机器满载时会被系统丢事件打红
  (实测 load 165 时 B/D 丢字、空载全绿)——先看 `uptime` 再判断是被测坏了还是负载拖的。

自测第 5 步改为按返回码语义断言(rc!=1 = 没复用;身份不可用时 rc=2 也算没复用)。

真机:整脚本一次 PASS(复用路径 logged「注入器复用现有 bundle」不重签);
戳为
`<sha>\tApple Development: qingfeng gao (7L8FV63FAP)\tsigned:Apple Development: qingfeng gao (7L8FV63FAP)`。

Refs #58
@gqf2008

gqf2008 commented Sep 16, 2026

Copy link
Copy Markdown
Owner Author

主仓已迁到 walgit(用户指示:vdev 后续在 walgit 维护,GitHub 仅作镜像发版),本 PR 的合并改在 walgit 侧完成,不再在 GitHub 合并(否则会与镜像循环打架)。

  • walgit 主仓:http://127.0.0.1:8081/gqf2008/vdev.git,collab 线程 hid-tcc-injector-persist-58
  • 分支 fix/hid-harness-58 已在 walgit 合并进 main(merge commit 4dd6e1a),已推 origin(walgit),GitHub 由镜像循环自动同步
  • 复审:独立复审第三轮 通过(无阻塞项)(review 条目已记入 collab 线程)

关闭本 PR(指针式)。

@gqf2008 gqf2008 closed this Sep 16, 2026
@gqf2008
gqf2008 deleted the fix/hid-harness-58 branch September 16, 2026 10:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🔄 [处理中][wt-hid-harness] [验收工具] HID 注入器 app 每次运行都被重写二进制 → 人工 TCC 授权跨运行失效(D 段)

1 participant