信任与签名
框架允许第三方扩展进入同一运行环境,因此必须先回答一个问题:这次装载的代码是谁发布的,内容有没有被改过? 签名负责回答这个问题;它不负责回答「这份代码安不安全」。
相关页面:装载管线与 guard、policy 与写边、包清单与目录、装载原因码。
签名 ≠ 可信
| 签名回答 | 签名不回答 |
|---|---|
| 这个包是否由它声称的发布者发布 | 这份代码是否安全 |
| 归档内容是否被第三方改动过 | 发布者是否不会作恶 |
| 出问题时责任归属于谁 | 这项操作是否可以放心执行 |
官方清单内、签名有效的包同样可以作恶。 签名让责任归属明确,是准入的必要条件,不是安全保证。任何「签名通过,所以可以……」「官方签过,所以安全」的论证都不成立。
因此,签名之外还有三层约束:release 构建的代码单元必须签名、敏感能力必须逐项同意、信任状态变更必须向用户完整展示。
信任链
构建期内嵌的根公钥
│ 校验
▼
官方签名清单(内建副本 + 经授权更新)
│ 登记发布者名、公钥与指纹
▼
扩展单元
├─ 自带发布者公钥
├─ 用该公钥验签
└─ 计算公钥指纹
│ 指纹比对
▼
guard 判定 ──► 直接装载 / 用户裁决 / 拒绝| 环节 | 规则 |
|---|---|
| 根公钥 | 构建期内嵌进产物,不随更新改变;框架与清单都不持有私钥 |
| 清单签名 | 官方清单本身由根密钥以 Ed25519 签名,验签失败即不采用 |
| 发布者公钥 | 清单登记发布者名、公钥与指纹;公钥与指纹不一致即清单无效 |
| 单元签名 | 单元自带发布者公钥;框架用它验签,再以公钥指纹核对官方清单或本地记录 |
私钥只在发布者手里。框架只做验签,不能替任何人签发,也不能把一份新清单「自立」为信任根。
四态判定
四种情形必须严格区分,不得合并,也不得用一种提示代替另一种。
| 情形 | 含义 | 处置 |
|---|---|---|
| 签名核验通过,且在官方清单内 | 发布者已由官方清单登记,内容未被改动 | 直接装载 |
| 签名核验通过,但不在官方清单内 | 签名有效,但该发布者尚未被本框架信任 | 显式提醒用户,用户同意后装载(TOFU) |
| 签名核验失败 | 归档内容与签名不匹配,极可能已被第三方改动 | 拒绝装载 |
| 未签名 | 没有可核验的签名信息 | release 构建的代码单元拒绝;debug 构建允许;数据单元按未签名批准流程处理 |
铁律:签名核验失败 ≠ 未签名 ≠ 不在清单内。 「核验失败」声明的不是「这是第三方开发者的原始包」,而是「这个包极可能被第三者动过」。把三者合并成一句「签名有问题」,会让用户失去最关键的判断依据。
同名异钥不是第五种签名状态。 它发生在签名核验通过之后,是「发布者名与本地记录或官方清单不一致」的冲突;处置属于 TOFU 裁决,不改变签名本身的四态分类。
补充规则:
- 代码单元(插件、Mod)在 release 构建必须签名;debug 构建允许未签名,供测试使用。
- 数据单元(数据包、资源包)不要求签名。攻击面由「安全关键 UI 不受数据单元影响」消除,而不是由「给每个资源包签名」消除。
- 已签名的单元如果公钥被官方清单撤销,新装载被拒绝;已装载单元继续运行,不因本次撤销卸载或撤销作用域。更高序号的清单可以恢复该密钥。
- 清单过期不是验签失败:离线场景继续沿用已验签的清单,同时在界面上提示过期。
官方签名清单
| 属性 | 要求 |
|---|---|
| 算法 | Ed25519 |
| 根公钥 | 构建期内嵌;更新清单不能替换根 |
| 序号 | 单调递增;序号不大于当前值的清单一律不接受 |
| 防回滚 | 以已接受清单的序号为基线,旧清单不能覆盖新清单 |
| 撤销 | 按公钥指纹登记;只拒绝新装载,不影响已装载单元 |
| 过期 | 记录签发与过期时间;过期只提示,不丢弃已验签内容 |
| 更新范围 | 只替换官方清单,保留个人信任记录、未签名批准与能力同意 |
| 发布范围 | 清单只登记官方及官方信任的开发者;其他开发者不受限制,只是装载时需要用户同意 |
清单更新入口只交给内置外壳,不能由普通扩展调用。个人信任记录与官方清单是两套数据:官方清单回答「谁被官方登记」,本地记录回答「用户自己信任过谁」。
TOFU:首次信任与唯一映射
TOFU 指「首次遇到时记录,此后按记录比对」。它不是「第一次见到就永久放行」。
| 规则 | 说明 |
|---|---|
| 一个发布者名只允许一个签名 | 首次装载时记录 发布者名 → 公钥指纹 |
| 同名异钥必须打断 | 后续单元声称同名但指纹不同,立即挂起并显式询问,禁止静默装载 |
| 冲突期间老人照常 | 已记录密钥的记录继续有效;新来者挂起等待裁决 |
| 裁决必须给出完整信息 | 两个公钥的完整指纹、各自的时间信息(无记录时明确写出)、各自是否在官方清单内 |
| 覆盖必须留痕 | 用户选择「信任新密钥」时覆盖旧记录,并把覆盖动作写入信任留痕 |
| 撤销入口 | 用户可清除某发布者的本地记录;此后该发布者的新装载重新按首次遇到处理 |
| 已装载不受影响 | 覆盖或清除记录都不卸载已装载单元,也不撤销其作用域 |
冲突界面必须明确标注哪一方「名单内」、哪一方「名单外」。这是用户在「攻击者抢先注册同名」场景下唯一可用的判断依据。
已知代价(明确接受):如果攻击者抢先,正确的扩展会被判为冲突而无法直接进入。此时界面必须指出「你安装的内容可能有问题」以止损——这是设计意图,不是缺陷。
身份与显示名分离
| 规则 | 要求 |
|---|---|
| 身份 = 公钥指纹 | 任何安全判定都不得依赖人类可读名称 |
| 显示名只用于展示 | 取自 manifest 的已签名字段,因此不可被第三方篡改 |
| 未签名单元必须标注 | 显示名旁明确写出「未签名,名称未经核验」 |
| 名称比较在 guard 内 | 规范化与比对逻辑不暴露给任何单元,避免被离线试探 |
显示名展示前必须经过名称检查:
- Unicode NFC 规范化;一切比较都在规范化之后进行。
- 拒绝控制字符、零宽字符与 Bidi 控制符;它们不可见或会改变显示顺序。
- 做同形异义检测与混合脚本检测;
1/I/l、拉丁与西里尔混排都不得伪装成已存在的发布者名。 - 与既有发布者名视觉不可区分的名称一律拒绝。
- 发布者名字符集受限;非 ASCII 名须额外校验,并展示规范化等价形式。
- 规范化实现由框架提供,不得把单元数据当作规范化输入。
未签名包与重新批准
未签名包按「稳定 ID → 归档 hash」记录批准。hash 一旦变化,必须重新询问,不能沿用旧批准。
| 规则 | 要求 |
|---|---|
| 适用范围 | release 下的未签名数据包与资源包,以及 debug 下的所有未签名单元(代码与数据);release 下的未签名代码单元直接拒绝 |
| 首次批准 | 记录归档 hash、批准时间与完整文件表 |
| 变化后批准 | 展示旧 hash → 新 hash、文件修改时间、写入方、逐文件变化清单 |
| 写入方 | 可查时必须展示文件系统所有者、进程名或来源及其证书主体;不可查时必须写「未知」 |
| 逐文件 diff | 必须逐文件列出新增、删除、修改,并区分可执行文件与普通文件 |
| 归档 hash | 覆盖包内全部文件(含清单与签名文件)的相对路径与 SHA-256 表,经规范化后计算;重打包时间不参与 |
为什么必须逐文件 diff:恶意改动最可能趁用户主动修改扩展时混入。此时用户警惕性最低,且期待「内容当然会变」。只给一个 hash 变化,等于让用户在没有证据的情况下点同意。把「只改了一个文本」与「改了一个 .js」明确分开,用户才有判断依据。
知情权是宪章级要求
凡涉及信任状态变更,必须向用户展示对象身份、时间、来源与变更内容;不得以「已处理」「已自动核验」代替展示。
| 情形 | 必须展示 |
|---|---|
| 签名冲突 | 两个公钥的完整指纹、各自的时间信息(无记录时明确写出)、各自是否在官方清单内 |
| 未签名包更新 | hash 变化、修改时间、写入方、逐文件 diff |
| 孤儿清理提示 | 单元名、稳定 ID、最后认领时间、占用、数据摘要 |
| 装载需用户裁决 | 发布者、指纹、能力清单、要改写的 trusted 目标 |
任何实现都不得以「界面简洁」「减少打扰」为由削减上述信息。安全关键 UI 同时受另一组约束:
- 只用随产物内置的字体与样式,不经资源管线取任何资源。
- 不得由任何扩展单元渲染或替换。
- 资源包中一旦出现保留的安全 UI 命名空间文件,整个包拒绝装载。
理由很直接:恶意字体可以用同形字形篡改开发者名与指纹的显示效果;能替换该界面的单元,就能伪造用户据以作决策的信息。
信任存储放在哪里
信任存储不属于扩展体系。 它记录的是信任决策本身,扩展若能读写,就能为自己伪造批准或篡改他人记录。
| 约束 | 内容 |
|---|---|
| 独立数据库 | 位于框架之外,独立于 Cordis 服务空间 |
不注册进 ctx | 装载器经宿主绑定直接访问,扩展拿不到 |
| 不参与热重载 | 不受 effect 管理,不随扩展卸载回滚 |
| 只给相关快照 | 装载器只能读取与待判单元相关的发布者记录、批准与同意,不能读取整库 |
| 写入只由用户触发 | 用户在信任页点击后由信任页写入;装载器只接收裁决结果 |
平台差异:
| 宿主 | 存储位置 | 强度 |
|---|---|---|
| Tauri | 由 Rust 侧持有,JS 侧没有访问路径 | 强 |
| 纯浏览器 | 独立源的存储(与外壳不同源) | 中等:同源存储必然可被同 realm 的扩展代码读写 |
| 两者都不可用 | — | 必须显式声明为弱保证,且不得据此宣称任何安全性 |
信任存储是「插件之间只经内核交流」的唯一例外。原因是它不受 effect 管理、不参与热重载;若走内核通道,它会变成可注入、可被 Mod 拦截的服务,扩展就能伪造批准或篡改记录。
信任页调用期限
非交互调用 signingList、updateSigningList、snapshot 与 theme 每次最多等待 10 秒;超时即拒绝并删除该调用的待处理状态,迟到应答不会解析成后续调用的结果。decide 与 manage 属于用户点选操作,在信任页仍处于活动状态时不设自动期限,直到用户响应或页面关闭。
当前状态
| 能力 | 状态 |
|---|---|
| 四态判定、同名异钥冲突、TOFU 首次记录与撤销 | 已完成 |
| 官方清单根验签、序号防回滚、撤销与过期提示 | 已完成 |
| 未签名单元批准、hash 变化重新批准与逐文件 diff | 已完成 |
| 独立源信任页与安全关键 UI | 已完成 |
「已完成」指网页与宿主两端的契约用例都通过。正式根公钥与官方清单由维护者生成并入库,属于发布流程,不由文档或测试代替。实现与测试证据见当前状态与测试。
参见
- 装载管线与 guard:四态判定在装载流程中的位置与原因码。
- policy 与写边:能力同意与 trusted 目标。
- 分发包与包结构:归档 hash 覆盖哪些文件。
- 隔离运行:不可信输入的另一条边界,与签名互不替代。
- 装载原因码:
GUARD_SIGNATURE_INVALID、GUARD_PUBLISHER_KEY_CONFLICT等结果的释义。