Be11aMer/beweiskette
GitHub: Be11aMer/beweiskette
一款零依赖的纯浏览器端数字证据保管链应用,通过 SHA-256 哈希链和可选的 RFC 3161 时间戳为数字文件构建可离线验证的防篡改记录。
Stars: 0 | Forks: 0
# 证据链
用于数字证据的加密保管链 —— 完全在客户端运行。
## 这是什么
Beweiskette(*德语:“证据链”*)是一个零依赖的 Web
应用程序,用于构建数字文件的防篡改记录。您注册的每个
文件都会被哈希处理、描述,并链接到哈希
链中的前一条记录。更改或重新排序任何过去的记录都会导致验证失败,且精确到
该条目。
**您的文件永远不会离开浏览器。** 所有 SHA-256 哈希处理都
使用 Web Crypto API 在客户端进行。没有后端,没有分析和没有
遥测。应用程序不会自行发出任何网络请求;唯一的例外是
您刻意触发的可选可信时间戳查询,该查询仅会发送
链头的 32 字节摘要,不包含任何其他内容。
MIT 许可证。尽情 Fork、扩展和在此基础上构建吧。
## 它能证明什么 —— 以及不能证明什么
本节非常重要。保管链
工具的大部分价值来自于确切了解其保障范围在何处终止。
**它能证明**,对于您已持有的链:没有任何记录被更改,没有任何
记录被重新排序,没有任何记录从中间被移除,并且
这些记录都属于同一条链。验证过程会重新计算每个
哈希,而不是比较存储的值。
**它本身不能证明 *何时*。** `timestamp_registered` 来自于
注册机器的时钟,而时钟是由制作记录的人控制的,因此
一条日期为去年的链与今天早上在日期倒退的笔记本电脑上构建的链
无法区分。从 API 获取时间并不能解决这个问题 ——
无论来源如何,未经签名的时间断言都是无法验证的,因为
验证者无法区分“是应用程序获取了这个时间”还是“是生产者手动输入的”。
真正能解决这个问题的是*由时间源签名*的值:请求一个 RFC 3161
时间戳,权威机构会将其时钟读数与您的头部哈希
一起签名,然后任何人都可以进行检查。
**它不能证明 *完整性*。** 从导出的链中删除最后三个条目,其余部分
仍然会验证为 INTACT(完整)—— 文件内部没有任何内容
承诺它原本应该有多长。
**它不能证明 *真实性*。** 没有签名,并且其构造是公开的。任何人
都可以生成一个包含任何内容的、完整且内部有效的
链。“CHAIN INTACT”的意思是*此文件在内部是*
一致的*,而不是*此文件是真实的*。
**它不能证明记录是 *真实的*。** 保管人、案件参考和
备注都是自由文本,由输入它们的人自行声明。
前三个缺陷可以通过**锚定**来弥补:在您无法控制其时间轴的
某处发布一个简短的头部回执,该链就会绑定到一个您无法
悄悄修改的时刻。参见 [docs/ANCHORING.md](docs/ANCHORING.md)。
完整的分析在 [docs/THREAT_MODEL.md](docs/THREAT_MODEL.md) 中;在依赖它
处理任何重要事项之前,请先阅读此文。
## 适用人群
- 记录证据处理过程的**法医调查员**
- 确立接收源材料时间的**记者**
- 维护案件文档文件完整性的**律师**
- 为漏洞发现添加时间戳的**安全研究员**
在每种情况下,该工具都提供了一份可辩护的内部记录。如果需要让时间戳
经受住质疑者的挑战,请将其与已发布的锚点结合使用。
## 工作原理
1. **注册** — 拖入一个文件。浏览器计算其 SHA-256,提取
EXIF 元数据(仅限 JPEG),并记录您提供的保管信息。
2. **链接** — 每个条目通过对规范编码进行 SHA-256 计算,提交给
其自身的内容、其位置 (`seq`)、其链身份 (`chain_id`) 以及
上一个条目的哈希。第一个条目链接到 `GENESIS`。
3. **锚定** — 复制头部回执并发布。这就是将
“内部一致”转换为“在特定时刻之前已存在”的过程。
4. **验证** — 导入一条链,每个哈希都会被重新计算,每个链接都会被
检查。粘贴已发布的回执可额外检测从末尾
删除的条目。
5. **导出** — JSON(记录)、CSV(有损摘要,**不可**验证 ——
它省略了元数据),或一个包含自身
验证代码并可离线运行的、自包含的 HTML 报告。
## 链格式 v3
条目的哈希是基于规范的 JSON 编码计算的 —— 键按 Unicode
码点排序,无无关紧要的空白字符:
```
{
"schema_version": 3,
"chain_id": "3f2a1b8c-4d5e-4f60-8a1b-2c3d4e5f6071",
"seq": 0,
"id": "…",
"timestamp_registered": "2026-07-24T09:31:04.117Z",
"evidence": { "file_hash": "…", "file_name": "…", "file_size": 204800, … },
"metadata": { "camera_make": "Canon", "gps_lat": "52.520008", … },
"custody": { "custodian": "…", "case_reference": "…", "notes": "…" },
"time_bound": { "beacon": { "source": "…", "pulse_index": 1234567,
"chain_index": 1, "output_value": "…",
"pulse_time": "2026-07-24T09:30:00.000Z" } },
"prev_hash": "GENESIS"
}
```
`entry_hash` 正好是这些字段的摘要,并且不属于其自身的
原像。该列表是一个**白名单**,而不是“除 `entry_hash` 外的所有内容” ——
黑名单会让条目碰巧携带的任何意外键悄悄加入到
摘要中,使得哈希提交的内容取决于对象的形状,
而不是格式。编码器在任何无法明确表示的值面前会**抛出异常**,
而不是进行猜测;一个悄悄进行强制转换的规范化器就是
一个碰撞生成器。
`time_bound` 是 v3 版本的新增内容:一个建立*不早于*概念的随机信标脉冲。
它被刻意放在摘要内 —— 事后附加的边界
什么也证明不了,因为一旦知道了期望的答案,就可以选择它。当没有记录脉冲时,它为 `null`。
## 安全属性
| 属性 | 状态 |
|---|---|
| **仅限客户端** | 文件永远不会离开浏览器。没有自发的网络请求;唯一存在的是您触发的可选时间戳查询,携带 32 字节摘要。 |
| **内容完整性** | 对任何条目的任何更改都会破坏该条目的验证。哈希是重新计算的,而不是比较的。 |
| **顺序完整性** | `seq` 包含在摘要中;可直接检测重新排序和链中删除。 |
| **链身份** | `chain_id` 防止将两条链中的条目拼接到一个文件中。 |
| **单射编码** | 规范编码器会拒绝它无法明确表示的值,而不是将它们折叠在一起。 |
| **输出转义** | 默认情况下,通过自动转义模板对来自导入链的所有值进行 HTML 转义。 |
| **CSV 公式注入** | 以 `=`、`+`、`-`、`@` 开头的字段会被中和;所有字段都被引用。 |
| **单一验证器** | 导出的报告内联了应用程序的规范化器而不是副本,因此两者不会产生分歧。 |
| **严格 CSP** | `default-src 'none'`,无内联脚本,无内联样式,无 `unsafe-eval`。参见 `public/_headers`。 |
| **尾部截断** | 没有已发布的锚点回执则**无法**检测。 |
| **整条链伪造** | **无法**检测。没有签名。请使用锚点。 |
| **可信时间戳** | 可选的 RFC 3161 锚定。Token 在浏览器内验证 —— 消息印模绑定、签名的 messageDigest、timeStamping EKU、有效期窗口以及 CMS 签名 —— 均针对**固定**的签名者密钥。使用 `openssl ts` 生成的 Token 可以直接粘贴,这是针对不发送 CORS 标头的权威机构的有效路径。 |
| **信标下限** | 记录在 `time_bound` 中的 NIST 随机信标脉冲证明了条目是*不早于*该脉冲制作的。结合锚点的*不晚于*属性,条目被限定在一个范围内,而不仅仅是单边限定。 |
| **自断言时间** | `timestamp_registered` 本身无法证明任何事。时钟属于制作记录的人。 |
关于常数时间比较:`constantTimeEqual` 被一致使用,但
在此处不应将其视为一种安全属性。这里没有秘密 —— 两个
操作数都已经公开 —— 而且 JavaScript 反正也无法保证常数时间
执行。这只是为了保持代码规范。
## 架构
```
src/
├── main.js App shell, tab routing, storage-durability check
├── crypto.js Hashing entry points; picks buffered vs streaming
├── sha256.js Incremental SHA-256, for files too large to buffer
├── canonical.js Canonical JSON encoder — the exact bytes that get hashed
├── chain.js Chain construction, validation, verification
├── anchor.js Head receipts, anchor records, coverage
├── asn1.js Strict DER parser/encoder (DER only, rejects BER)
├── rfc3161.js Timestamp requests and CMS signature verification
├── beacon.js Randomness-beacon lower bound
├── settings.js Timestamp authority URL and pinned keys
├── store.js IndexedDB persistence (raw API)
├── exif.js Minimal JPEG EXIF parser
├── report.js CSV and self-contained HTML report generation
├── utils.js Formatting, HTML escaping
└── ui/ register · chain-view · verify · export
```
`canonical.js` 是刻意自包含的:`report.js` 逐字嵌入了其源
代码,因此离线报告和应用程序不会产生偏差。
最大 64 MB 的文件使用 Web Crypto API 进行哈希处理。超过该大小的文件将
通过 `sha256.js` 进行流式处理,因为 `crypto.subtle.digest()` 需要
一次性将整个输入加载到内存中,而证据文件通常很大。经
验证,这两条路径针对 FIPS 180-4 向量以及直接针对 Web Crypto 都会生成
相同的摘要。
**零运行时依赖。** 原生 JavaScript,原生 CSS。Vite 仅用作开发
服务器和构建工具;测试使用 Node 的内置运行器。生产环境
输出为纯静态文件。
## 开发
需要 **Node 20 或更高版本**。测试套件需要 `globalThis.File` 和
一个能保留到测试运行器子进程中的 `globalThis.crypto`;
这两者在 Node 18 上都无法满足。CI 会在 20、22 和 24 版本上运行测试套件,因此声明的
最低版本要求是经过测试的,而不是假设的。
```
git clone https://github.com/Be11aMer/beweiskette.git
cd beweiskette
npm install
npm run dev # http://localhost:5173
npm test # node:test — no test framework dependency
npm run build # static output in dist/
```
在部署之前运行 `npm test`。测试套件涵盖了注入向量、
规范化冲突以及应用程序/报告验证器的一致性 —— 这些都是会
悄悄崩溃的地方。
### 部署
```
npm run deploy # Cloudflare Workers static assets, via wrangler.jsonc
```
部署为提供静态资源服务的 Worker,没有 Worker 脚本 ——
没有服务器端代码,这才是重点。
`public/_headers` 包含 CSP 和相关标头;Workers 静态资产
使用该文件而不是直接提供它,已通过 `wrangler dev` 验证。如果您
部署到其他地方,请对其进行适配 —— 并确认标头确实正在被
发送,而不是想当然。
## 与 Zeitkette 的关系
Beweiskette 是证据链,对应于
[Zeitkette](https://github.com/Be11aMer/zeitkette),后者是一个用于
加密验证工作时长记录的 CLI 工具。
| | Zeitkette | Beweiskette |
|---|---|---|
| **领域** | 工作时长 | 数字证据 |
| **接口** | Python CLI | Web 应用程序 |
| **存储** | JSONL 文件 + git | IndexedDB + JSON 导出 |
| **锚定** | Git 提交 | 已发布的头部回执 |
两者共享规范的编码。`test/vectors/canonical.json` 是共享的
测试夹具:13 个涵盖了实现实际会产生分歧的案例的向量
(非 ASCII 值、非 ASCII 键、基本多文种平面之外的键排序、转义)。任何
声称兼容的实现都必须准确重现每个 `canonical` 字符串
和 `sha256`。
Python 端必须使用:
```
json.dumps(value, sort_keys=True, separators=(',', ':'), ensure_ascii=False)
```
需要 `ensure_ascii=False`,这**不是** Python 的默认设置。使用
默认值时,Python 会将非 ASCII 转义为 `\uXXXX`,而 JavaScript 会输出原始的 UTF-8,
因此名为“Müller”的保管员在这两个工具中的哈希值会有所不同。
## 法律声明
Beweiskette 创造的是**完整性的证据**,而非法律确定性。哈希
链日志表明记录是按顺序创建的,并且自创建以来未被
修改。它不能确定它们的创建时间,也不能保证
它们是完整的或其内容是真实的。
- 此工具不构成法律建议。
- 证据价值取决于司法管辖区、背景以及任何
法律程序的具体情况。
- 在特定情况下,请咨询合格的法律专业人士。
## 贡献
欢迎提交 Issue 和 Pull Request,但请参阅顶部的状态说明 ——
回复可能会比较慢。安全报告:[SECURITY.md](SECURITY.md)。如果您正在
对其进行蓄意攻击,[docs/PENTEST.md](docs/PENTEST.md) 说明了哪些值得
您花时间,以及哪些限制是已知的。
## 许可证
MIT — 详见 [LICENSE](LICENSE)。
标签:MITM代理, Web Crypto API, 区块链/哈希链, 密码学, 手动系统调用, 数字取证, 程序员工具, 自动化脚本, 自定义脚本, 证据保全