# 证据格式

Canonical page: https://phishdestroy.github.io/TgWitness/zh/docs/reference/evidence/

签名 ZIP 文件的内容、记录类型、操作收据、用户资料、异地副本及离线验证。

观察结果不等于结论。发件人 ID 不等同于现实世界中的身份。 账户可见的服务更新是数据来源；Telegram 不会为第三方调查人员对这些保存的记录进行加密 签名。

## 什么是签名

| 资料 | 保护 | 应保留的内容 |
| --- | --- | --- |
| 日志中的每一项记录，包括操作事件 | 每个事件均包含 SHA-256 链和一个 Ed25519 签名 | 原始 `events.jsonl`（可信密钥指纹）及保留的检查点 |
| 证据 ZIP 文件内容 | `manifest.sig` 对规范清单进行签名，该清单列出了内容文件的哈希值 | 完整的 ZIP 压缩包，包含其清单和签名 |
| 保留的附件 | 带签名的`media.saved`事件中包含SHA-256和字节计数；带签名的ZIP清单中包含文件哈希值 | 经过验证的 ZIP 文件中的原始字节 |
| 检查点 | 对日志长度、头部哈希值、公钥及创建时间进行 Ed25519 签名 | 在发生争议性更改前保留的独立副本 |
| 运行收据 | 单独签名的采集器测量数据；也包含在经过签名的`process.started`事件中 | 收据及相关日志；相关数据均为用户自报 |
| 独立的 CSV 或 HTML 文件 | 不包含独立的加密签名；仅凭事件哈希值是不够的 | 请使用经过验证的 ZIP 文件中的原文件进行完整性校验 |
| 由浏览器生成的屏幕截图、打印件或 PDF 文件 | 衍生作品上不包含 TgWitness 的签名 | 保留原始的经过验证的软件包，并记录衍生版本的制作过程 |

该采集器在生成每种导出格式之前，都会验证日志签名。 该源数据验证不会在单独下载的 CSV 或 HTML 文件上生成签名。同样，有效的 JSONL 事件签名也不会提供附件 字节，也无法独立证明已提供了完整的观察日志。

## 调查移交

1. 请在案件笔记中记录案件编号、操作员、Telegram 账号数字 ID、选定的 信息源、采集政策及采集时间段。TgWitness 负责保存观察记录；案件分配和证据保管记录则在 应用程序外部进行管理。请通过 [实时验收流程](https://github.com/phishdestroy/TgWitness/blob/main/docs/internal/LIVE_ACCEPTANCE.md)确认预期的采集设置是否符合要求。
2. 在依赖 其导出数据之前，请独立保留工作区的公钥指纹。在收集过程中，将签名检查点保存至收集器之外。 记录中断情况以及该账户可见性的限制。
3. 导出**带签名的 ZIP** 文件并保留带签名的运行收据。请在 导出日志边界处或之前设置检查点。较晚的检查点会正确地拒绝 较早的导出，因为其长度不足。导出内容包含完整的工作区日志， 不受面板过滤器影响；请在分享前仔细审查相关材料。
4. 请收件人使用可信的验证器副本以及 独立提供的指纹/检查点来验证 ZIP 文件。请在已激活 Python 环境的可信代码检出环境中：

   ```
   python -m tgwitness.verify evidence.zip --fingerprint TRUSTED_PUBLIC_KEY_SHA256 --checkpoint retained-checkpoint.json
   ```

   当两个输入均已提供且被接受时，成功输出报告 `key_pinned: true` 和 `checkpoint_checked: true` 。保留验证结果。

   ```
   For a handover requiring the final exported record to be witnessed, use
   `--witness witness.zip --witness-fingerprint TRUSTED_WITNESS_SHA256 --require-witness`.
   This refuses an export with an unwitnessed tail. See the
   [witness handover procedure](WITNESS.md#require-a-witnessed-export-boundary).
   ```

   1. 请在保管记录中记录归档文件的 SHA-256 值、导出边界、传输日期、发件人和 收件人。请保持原始包的完整性，并仅对副本进行操作。 请分享证据文件，而非应用程序的私有数据 目录——该目录还包含账户会话和签名密钥。 传输时，可将导出文件放入受密码保护的 AES-256 ZIP 压缩包中。该 密码仅用于保护机密性，且不会被存储；接收方解压 存档后，可验证其中未被篡改的签名导出文件。
   2. 如果需要独立的时间锚定，请遵循 [RFC 3161 流程](https://phishdestroy.github.io/TgWitness/zh/docs/reference/timestamp/)，并保留请求、检查点、可信 CA 信息以及经过验证的权威机构响应。仅准备请求 并不能获得时间戳。

成功的验证可确立相对于可信收集器 密钥和保留边界的完整性。原始真实性、完整的 Telegram 消息传递、可信时间 以及法律可采纳性均需单独证据支持。本版本尚未完成 Telegram 实时环境认证或独立的法证/安全评估。

被复制的收集器密钥可以对伪造的替代日志进行签名。通过 与独立保留的检查点或见证记录进行验证，可以检测到早期前缀的变更； 仅靠签名验证无法做到这一点。 “见证”仅对采集器提交的内容进行确认，因此 无法独立证实首个 外部检查点之前伪造记录的真实性。云部署报告会检查配置及采集器报告的 测量数据；但无法证明在初始化过程中私钥从未被复制。

通过 Apprise 发送的通知包含工作区和聊天群组名称及数量；已删除和已编辑消息的简短 摘录仅在操作员启用该功能时才会添加 （默认关闭）。这些仅作为便利提示，而非证据：无法保证消息送达， 且只有日志及其签名导出文件才会记录实际观察到的内容。 若启用工作区 代理，通知将通过相同路径传输；无法使用该路径的服务以及 MTProxy 路由将被拒绝。

## 事件规范化

哈希输入必须包含以下字段：

```
seq, observed_at, source_at, kind, chat_id, message_id, actor_id, payload, previous_hash
```

使用与 Python 兼容的 JSON 格式进行序列化，采用 UTF-8 编码，对象键按顺序排列，不进行 ASCII 转义，不包含无关的空白字符，且不包含 NaN/无穷大：

```
json.dumps(value, ensure_ascii=False, sort_keys=True,
           separators=(",", ":"), allow_nan=False).encode("utf-8")
```

这些字节的 SHA-256 值即为事件哈希。签名采用 Ed25519 对 32 个原始哈希字节进行计算，随后进行 Base64 编码。第一个 previous_hash 由 64 个零组成。 序列号从 1 开始，并以 1 为增量递增。所有事件（包括操作性 事件）共用一条链。系统不抑制重复交付：重复 的观测结果是独立的收据，而非对独立 Telegram 操作的声明。

Telegram 的原始日期为 ISO 字符串；二进制字段将转换为包含 bytes_base64 的对象。消息正文、实体、媒体描述符、回复元数据、点赞、 以及服务操作（如有提供）均保留在原始消息元数据中。

observed_at 是采集器的 UTC 墙钟时间。source_at 是提供的 Telegram 日期 （如有）。删除通知不会提供确切的删除时间，因此 source_at 保持为空。传输接收顺序不一定与源事件 顺序一致。历史快照使用较早的源日期，但采用当前的观察时间。

## 行为主体语义

message.new 中的 actor_id 即为消息发送者。message.edited 未推断出 编辑者；payload.sender_id 记录了被观察消息的发送者。 删除 actor_id 保持为 null。成员服务消息保留发送者和操作 类型，不会将每次移除都转换为自愿退出。原始参与者 更新保留所提供的 actor_id。反应快照不会虚构变更操作者。

私人聊天和普通群组的删除通知仅包含消息 ID。 这些聊天共享一个全账户通用的 ID 序列，因此一个 ID 代表该账户中的一条 消息。 当具有该 ID 的消息此前已保存在此日志中时， 删除操作将在该聊天中记录为 message.deleted，并使用 payload.chat_resolution 作为方法名称。从未被保存过的 ID 将作为 deletion.unscoped 保存，且不包含 chat_id；系统不会进行任何推测。 频道和超级群组按聊天对消息进行编号， 绝不会通过这种方式进行匹配。当被删除的消息是由被观察账户发送时， payload.deleted_message_was_outgoing 为 true；但仍未提供删除者信息。

## 覆盖范围：连续性、缺失内容及未被观察到的 ID

每个签名 ZIP 文件都包含 `coverage.json`，HTML 报告以相同的摘要开头，而 `verify.py` 会根据 `events.jsonl` 重新计算该摘要，而非直接信任该文件：

- **从 X 到 Y 持续进行观察。**一个时段以 `observation.started` 开始， 并以暂停、停止、Telegram 连接中断、注销、进程启动但未 有序停止，或在观察期间超过三分钟无任何记录而结束 （每分钟都会写入一次心跳记录，因此静默意味着进程停止、卡死或 计算机进入睡眠状态）。当在**“参数**”中更改心跳间隔时，每次心跳 都会记录该变更，且允许的静默时长变为三个间隔，但绝不少于三分钟。
- **“缺口”**列出了各时段之间的时间间隔及原因。若最后 一条记录处仍存在缺口，则标记为 `not_resumed_by_last_record`。
- **未检测到的消息 ID。**对于按 聊天计数的消息的频道和超级群组，已检测到的消息 ID 之间的缺失 ID 将以范围形式列出。这些消息会被报告为 “未*检测到*”——即已被删除、从未送达或超出保存的历史记录范围——绝不会被标记为 “已删除”。 私聊和普通群组共享全账户范围的序列号，因此其中的 缺口没有任何意义，故不予列出。
- 聊天层面的数据缺失通知（例如 Telegram 在某个 频道中要求恢复差异数据）被计为注释；它们不会中断全局覆盖。

“覆盖范围”描述了该采集器记录的内容。这并不证明 Telegram 在 连续期间内发送了每一条更新。

## 制作人

证据 ZIP 包、聊天记录导出、Witness 日志以及 人员档案文件的声明中包含的签名清单包含 `producer` ：软件、版本、项目（PhishDestroy）、 主页和源代码仓库。HTML 报告中显示了相同的行，`verify.py` 将其作为已验证内容的一部分输出。 在此字段存在之前生成的归档文件仍按 原有方式进行验证，并在报告中显示为未指明生成者。

## 人物简介

应请求，系统会基于日志中已有的 某次聊天的记录生成个人资料文件（`tgwitness-dossier-v1`，JSON）；该文件不会从 Telegram 获取任何数据，且个人资料内容绝不会 写入日志（个人资料的启用和停用将记录为 `dossier.changed`）。 对于每位拥有已保存消息的用户，该文件会列出其姓名和用户名历史记录、Telegram标记 （机器人、诈骗、虚假、高级、已验证、受限、已删除）——以保存时显示的状态为准，以及个人资料 照片ID、来自本地头像缓存中与该头像相似的头像的SHA-256和感知哈希（pHash，与`imagehash.phash`兼容） 、活动记录（首条和末条消息、 数量、已编辑和已删除的分享、按UTC小时划分的消息）、谁回复了谁、该用户曾在此日志中参与的其他聊天 中该用户曾发帖的聊天记录，以及被提及的钱包（BTC、EVM、TRON、TON、LTC、 DOGE、XMR、SOL；Base58Check地址已通过校验和验证）、电话号码、用户名和 链接，包括隐藏在文本背后的链接。

每个事实都会列出其来源的日志记录（`seq`），而 `record_hashes` 则包含 每个被引用的记录的哈希值。该声明使用工作区密钥进行签名：

```
python verify.py tgwitness-dossier-CHAT.json --fingerprint TRUSTED_SHA256 --evidence tgwitness-evidence.zip
```

该工具会检查签名，验证证据导出文件本身（包括签名、哈希链和文件）， 并确保每个被引用的记录在同一工作区的导出文件中完整无缺且未被修改。头像哈希值会被标注为来自本地缓存 （而非日志记录）。提及内容属于模式匹配——仅为待核查的线索，而非已验证的事实。 配置文件按聊天进行启用，可导出为带密码的 AES-256 压缩包，且 在关闭配置文件或聊天从来源中移除时被删除。

## 独立见证者

将证人日志（`tgwitness-witness-v1`）及其频道帖子与 通过 `verify.py --witness … --channel …` 导出的证据进行比对；参见[独立证人](https://phishdestroy.github.io/TgWitness/zh/docs/reference/witness/)。

## 清单与检查点

manifest.json 包含格式、导出时间、公钥、指纹、日志 长度、头部哈希、范围、生成者、限制条件，以及除 `manifest.json` 和 `manifest.sig` 本身之外 每个内容成员的 SHA-256 值。 manifest.sig 是规范化 manifest JSON 的 Base64 编码 Ed25519 签名。 验证器会拒绝处理意外/缺失的成员、名称重复、文件损坏、 序列/链断裂、签名无效以及与提供的检查点不匹配的情况。 它绝不会提取归档路径。它将声明的总未压缩大小限制为 4 GiB； 若提高 **Parameters** 限制，`--max-gib N` 允许创建更大的归档文件。

请从可信的代码检出或经过审核的独立副本中获取验证程序代码。位于不可信存档中的验证程序 是由该存档作者提供的可执行内容； 其存在仅是为了便于移植，而非独立的信任锚点。

检查点会签署一份声明，其中包含公钥、日志长度、头部 哈希值以及创建时间。验证者可以检查后续存档是否包含 相同的前缀。在发生有争议的更改之前，请将检查点保留在收集器之外： 如果攻击者同时控制了存档和检查点，则可能同时篡改两者。

来自未固定密钥的有效签名仅能证明内部一致性。 即使使用固定密钥，如果采集器或密钥在记录前已遭 泄露，也无法证明源内容的真实性。外部时间戳和可信捕获是 独立的要求。TgWitness 不保证证据的可采纳性或法证认证。

## 附件

文件在 media/ 目录下通过 SHA-256 哈希值进行标识。media.saved 事件将哈希值和 字节长度与特定的消息事件相关联。原始文件名属于元数据，绝非本地 路径。 媒体数据以异步方式下载，因此立即删除可能导致 无法捕获。已禁用的采集、超大/未知大小的文件、下载失败 以及下载中断等结果均会显示。进程崩溃可能会留下 未被引用的部分文件；这些文件不会包含在导出数据中。

ZIP 导出工具会根据文件名对存储的媒体文件进行验证。JSONL、CSV 和 HTML 导出格式会验证事件签名，但不包含媒体文件数据。衍生报告 仅供参考；events.jsonl 才是权威的事件表示形式。

## 持久的收集与处理收据 (0.2)

媒体任务及其消息事件提交在同一事务中完成。 该任务存储 原始 TL 消息描述符；签名的观测记录将其 SHA-256 值、账户 及大小策略进行绑定。恢复操作会在下载前验证该绑定。它绝不会获取 当前消息，也不会悄无声息地替换为更新版本的附件。中断的任务 会重试，而永久性失败则仍被视为明确的结果。

每个工作区都有独立的日志和签名身份。process.started 事件包含 运行 ID、本地时间、依赖项版本、源文件哈希值、初始账户/范围， 以及捕获设置。心跳消息包含运行 ID、单调时间以及队列状态。 该流程收据也可作为单独签名的声明下载。 文件测量数据为自我报告数据，并非可信的硬件认证。

自动检查点会在启动时、监控期间每五分钟（ **参见“参数 → 每隔**”）、以及有序停止时本地保留。 启动验证和导出操作会将日志 与最新保留的本地检查点进行比对。当检查点完好无损时，此机制可检测到数据库回滚； 但若操作员同时控制两者，仍可将两者一并替换。 独立的保留机制和外部可信时间戳仍是各自独立的要求。

重启后，将恢复已保存的 Telegram 更新位置（`catch_up_from` 位于 `observation.started`），并请求获取遗漏的更新。定期重新检查会记录 `message.recovered` 以获取遗漏的消息，并在 Telegram 不再 返回已保存消息时记录 `message.missing_on_recheck`；后者并非删除通知，且未指明删除者或时间。

### 异地备份与恢复（S3）

异地副本（“导出”，默认关闭）会将日志片段、保留文件、检查点 及其时间戳，以及已完成的聊天记录导出上传至兼容 S3 的存储空间，上传前使用 AES-256-GCM 加密，所用密钥存储于工作区保险库中；若存储桶支持，则应用 对象锁定保留机制。若未下载密钥，则无法读取备份内容。请使用 `python -m tgwitness.replica restore --key-file KEY.json --out DIR` 进行恢复和验证。

恢复操作需要一个空的普通目录。下载的文件和解密后的对象将保存在私有 临时存储中，直至整个日志及所有保留的检查点均通过验证。不安全的对象 路径、不一致的段范围以及长度短于检查点的日志将被拒绝；验证失败 时，系统不会发布任何恢复文件。 保留性检查会将返回的“对象锁定” 模式和日期与上传时发送的截止时间进行比对（允许一秒的四舍五入误差）。过期、 无效或被缩短的截止时间均不视为已验证。状态报告将显示该上传批次中 被读取保留状态的抽样对象。

复制进度以目标位置和加密密钥为范围。更改端点、存储桶 或前缀将触发将本地保留的日志、媒体文件和检查点完整复制到 该目标位置。旧的未限定范围的进度将被保留，但在升级后会重放一次。历史 聊天记录导出文件若已从本地发件箱中删除，仍会保留在其原目标位置。

缺失或损坏的本地媒体文件将作为当前迭代的错误报告，并保持媒体光标停留在 该文件之前，以便在下一次迭代中重新尝试处理该文件；其后的文件、检查点和导出内容仍将 被复制。恢复过程会 将所有已保存媒体的引用与解密后的哈希值及记录的大小进行比对。 来自中断上传的 相同重叠日志段将被去重；冲突的重叠部分将被拒绝。 上传过程中创建的检查点将推迟处理，直至其所涵盖的日志记录被 复制完毕，因此一个完成的处理阶段不会在日志之前发布检查点。

### 流式导出快照

流式导出使用稳定的 SQLite 读取快照。当下载完成时，该快照的起始点可能早于 当前的实时日志。在此快照之后创建的检查点 可能会正确报告存档长度较短；请与位于导出前缀处或之前的检查点进行比较， 而非任意的后续检查点。

## RFC 3161 准备与响应验证 (0.2)

证据模式（默认关闭）仅通过工作区路径（绝不通过 MTProxy）将每个签名检查点的 SHA-256 哈希值发送至列出的 RFC 3161 权威机构和 OpenTimestamps 日历。 响应结果保存在 `checkpoints/anchors/` 中，经 `verify.py` 导出并验证（`--tsa-ca` 负责 验证 TSA 签名；使用 `ots upgrade` 和 `ots verify` 确认 OpenTimestamps 证明）。

该面板会准备一个请求存档，其中包含规范的 checkpoint.json 字节数据、 SHA-256 消息指纹以及 timestamp.tsq 文件中的随机非ce值、校验和及操作说明。 此过程不会发起任何网络请求。该准备过程不构成可信时间戳。

`python -m tgwitness.anchor verify` 会调用 OpenSSL 的 ts 函数两次：一次针对检查点 字节，一次针对原始请求。必须显式指定受信任的 CA 文件； 内置证书不会被自动信任。在命令行界面（CLI）中，请求验证为可选操作， 因此请保留并提供 --request 参数以验证非ce。结果将表明 是否已进行验证。收集器的检查点签名和日志仍需 使用可信收集器指纹分别进行验证。

当前的验证器不会获取吊销信息，也不支持证书过期后的长期 归档验证。请勿将成功的签名 验证等同于合格的时间戳或法律可采纳性。在开发过程中，尚未联系过真正的独立 时间戳服务提供商（TSA）。请参阅[操作流程](https://phishdestroy.github.io/TgWitness/zh/docs/reference/timestamp/)。
