只读 Checksum 审计与可靠的 CLI 输出契约
本文是 MCLI 只读 checksum 校验流程,以及发布审查中发现的 non-TTY 输出缺陷 pgsty/mc#5 的设计与实现记录。
状态: 已随最终的 mcli 20260903 正式发布。命令经 pull request #8 与 #13 合入
main,在托管 CI 中针对真实 SILO 服务器验证, pgsty/mc#5 已关闭。把客户端打包进 Server 镜像仍是独立的后续门禁。
归属:pgsty/mc。
跟踪: pgsty/mc#5。
安全边界: 本命令只读校验,不负责修复。
太长不看(TL;DR)
历史 CopyObject 实现可能在转换后的存储字节上计算 additional checksum,而不是在
S3 返回给客户端的逻辑对象字节上计算。mcli checksum verify 会筛选对象,独立地
把逻辑对象流送入已记录的算法,并把每个候选分类为 MATCH、MISMATCH、
NO_CHECKSUM、WOULD_VERIFY(dry run)、十种 UNKNOWN_* 分类之一,或三种 SKIPPED_* 之一。
首版实现在终端中工作正常,但 stdout 被重定向时完全不输出。MCLI 为了在非终端 环境中禁用进度 UI,会自动把执行状态标记为 quiet;新命令错误地把这项内部状态 理解成了“用户要求隐藏审计结果”。修复将语义输出与进度抑制分离,同时不改变 全局 quiet 行为,也不会在 CI 中重新打开进度条。
命令与范围
V1 支持标记为 FULL_OBJECT 的 CRC32、CRC32C、CRC64NVME、SHA1 与 SHA256。
候选可以是单个对象、精确 VersionID、前缀下的当前对象、所有版本,或 JSON Lines
manifest 给出的精确条目。它还支持 SSE-C key 映射、时间与大小过滤、dry-run 成本
估计、有界 worker、下载限速、JSON 输出和可选的 JSON Lines report。
V1 不验证 COMPOSITE checksum,不从 ETag 推断类型,不读取 xl.meta,不能确定
历史 writer,也不会修复 metadata。端点必须随 checksum 一并报告其类型
(x-amz-checksum-type);对不报告类型的端点,每个带 checksum 的对象都会被归为
UNKNOWN_CHECKSUM_TYPE,而不是靠猜。
只读数据路径
对每个候选对象,MCLI:
- 使用 checksum mode 执行
HEAD,保留所有支持的 checksum 与ChecksumType; - 对不支持或含糊的状态返回
UNKNOWN_*,绝不猜测; - 将
GET返回的逻辑字节流送入有界 hasher,不把对象体写入磁盘; - 对固定版本使用 VersionID;对可变的未版本化/null 对象使用
If-Match,并在 读取后再次HEAD; - 将独立计算结果与已存值比较。
S3 边界只允许 LIST、HEAD 与 GET;如果 mock endpoint 收到写方法,测试必须失败。
结果与退出码契约
每个候选只产生一个稳定结果:
| 结果 | 含义 |
|---|---|
MATCH |
所有支持的已存 checksum 都匹配逻辑对象字节 |
MISMATCH |
至少一个已存 checksum 不同 |
NO_CHECKSUM |
没有 additional checksum,因此不读取对象体 |
WOULD_VERIFY |
dry-run 找到可验证的 full-object checksum |
UNKNOWN_* |
MCLI 无法给出可靠判断 |
SKIPPED_* |
过滤器主动排除了对象 |
summary 携带 objects、verified 计数、每种结果状态的计数以及 incomplete。
verified 等于 MATCH 加 MISMATCH:只有这两种结果真正把对象体流过了哈希器。
枚举了很多对象却一个都没核验的运行,会如实地显示出来。
--fail-on 支持 mismatch、unknown、no-checksum、any 与 none。默认 any
会在 mismatch 或校验不完整时返回 exit 1。no-checksum 在任一对象没有 checksum
或根本没有核验任何对象时返回 exit 1,因此空前缀或过期的 manifest 不可能冒充
一次干净的审计。dry-run 不应用 --fail-on。参数、认证、枚举与 report
写入失败属于命令失败,而不是对象分类。
其中,SKIPPED_TOO_LARGE 会让默认 any 返回 exit 1,因为大小上限使审计不完整;
时间过滤与 delete-marker skip 本身不会触发失败。
输出与自动化契约
对象记录和最终 summary 都是命令的语义输出:
- 除非调用方显式设置
--quiet、-q或MC_QUIET=true,TTY 与 non-TTY stdout 都必须收到全部对象记录和最终 summary。 - non-TTY
--json每行输出一个紧凑 JSON 值;TTY JSON 保留 MCLI 既有的美化格式。 - 全局参数在 app、
checksum与verify三层位置都必须生效。 --report独立于 stdout;即使显式 quiet 让 stdout 静默,它仍会写入对象记录和 最终 summary 的 JSON Lines。- 输出通道不会改变
--fail-on的判定。
这一区分之所以必要,是因为 MCLI 历史上的 globalQuiet 有两个来源:用户显式的
quiet 参数,以及拿不到终端尺寸时自动启用、用于关闭进度 UI 的 non-TTY 状态。
直接修改这个全局量,可能让 copy、get、put、mirror 等命令在 CI 中重新输出进度条。
最终修复只作用于 checksum 命令。它沿完整 CLI context 链查找显式 quiet/JSON
参数,因为 CLI 库的 GlobalBool 会停在最近的祖先 flag set;同时在 checksum action
内部恢复 JSON Lines,因为嵌套 Before hook 可能在 app-level --json 之后重置它。
其他命令的进度与输出行为均不改变。
Report、秘密与运行成本
Report 文件以 0600 新建,目标必须不存在;它只包含 metadata 与结果,不包含对象体
或 SSE-C key。Manifest 同样只保存 bucket、key 与可选 VersionID。
校验会下载每个受支持对象的完整逻辑内容。运维人员应使用 --dry-run、--max-size、
时间过滤、--max-workers 与全局下载限速控制成本和负载。NO_CHECKSUM 与
UNKNOWN_* 数量必须显式展示,二者都不能被包装成“校验成功”。
Mismatch 能证明什么
Mismatch 只能证明:校验时 endpoint 返回的 additional checksum,不能描述同一时刻 返回的逻辑对象字节。它不能单独证明对象一定由某个历史压缩缺陷生成,也不是与外部 真值的比较。
不要原地覆盖 checksum metadata。应先只读审计和分类。对已经确认且确有业务影响的
mismatch,优先写入新 key 或新 version,验证替代对象后再显式切换消费者;
UNKNOWN_* 对象不得进入自动修复。
验证记录与发布边界
本地验收矩阵覆盖 TTY human/JSON、non-TTY pipe、普通文件重定向、app/parent/leaf
三层 JSON 与 quiet、环境变量 quiet、quiet 下的 report、report 写失败,以及
MISMATCH/UNKNOWN 退出码。真实本地 S3 还覆盖了历史 MATCH、MISMATCH 与不支持
的 composite 对象。
该命令已随最终的 mcli 20260903 从 main 顶端的签名
tag 发布,功能套件 —— 包括针对真实 SILO 服务器的一次 checksum 校验 —— 对该提交
全部通过,pgsty/mc#5 已关闭。Server 内置
客户端与生产审计仍是之后需要独立证明的门禁。