samjanny/entangled-api
GitHub: samjanny/entangled-api
Entangled协议的Rust参考实现,用于构建和验证签名文档。
Stars: 0 | Forks: 0
# 混沌 API
[](https://github.com/samjanny/entangled-api/actions/workflows/ci.yml)
[](#license)
[](#install)
Rust 实现的混沌 v1.0 协议:类型化签名文档、封闭模式验证、JCS 正规化、Ed25519 签名和验证、发布者身份短语推导、Tor v3 原始地址绑定、金丝雀检查、客户端状态助手。
混沌是一个用于在敌对或匿名导向的载体网络上发布签名、结构化文档的协议。它旨在为小型内容站点设计,其中读者应能够验证发布者身份,同时客户端故意保持渲染攻击面狭窄。
使用混沌构建的站点不是一个网络应用程序。它是一组签名 JSON 文档,通过载体(如 Tor v3)提供并由专用客户端渲染。没有 JavaScript、没有 DOM 脚本、没有 HTML、没有 cookie、没有环境浏览器存储,也没有发布者控制的客户端界面。
## 状态
`entangled-api` 当前包含一个 Rust 包:
- [`entangled-core`](./entangled-core): 协议核心库。
当前包版本:`0.10.0`。
在 `entangled-core` 中实现:
- 清单、内容和事务文档类型。
- 混沌 v1.0 传输格式的封闭模式验证。
- 十一种签名内容块类型。
- 签名输入的 JCS 正规化。
- Ed25519 签名和严格验证。
- 清单、内容和事务的签名域分离。
- 发布者身份短语推导和恢复。
- Tor v3 葱地址解析和原始地址绑定。
- 清单类型状态验证管道。
- 金丝雀结构检查和金丝雀状态计算。
- 发布者历史检查的降级助手。
- 带策略感知助手的客户端状态存储。
- 提交体构建和验证。
此包的范围不包括:
- 网络传输。
- HTTP 客户端/服务器实现。
- 完整的混沌浏览器/客户端 UI。
- 信任状态持久化和 UI 面板。
- 发布者历史存储。
- 同意提示 UI。
- 图像解码和渲染。
这些预计将存在于更高级别的包或应用程序中。
## 混沌存在的理由
混沌将通常在网络上纠缠在一起的四个关注点分开:
1. **发布者身份** - 长期离线 Ed25519 身份密钥。
2. **载体可达性** - 如 Tor v3 葱服务之类的地址。
3. **常规发布签名** - 定期轮换的运行时密钥。
4. **文档渲染** - 由客户端约束的语法渲染。
目标是让读者验证文档是否属于同一发布者,即使服务器被破坏、原始地址轮换或载体迁移,同时避免通用浏览器运行时的攻击面。
混沌不是一个匿名层、网络替代品、分布式存储系统或否认机制。它依赖于所选的载体网络进行路由、可达性和任何网络层匿名。
## 仓库布局
```
.
├── Cargo.toml # Workspace manifest
├── Cargo.lock # Locked dependency set
├── deny.toml # cargo-deny policy
├── CHANGELOG.md
├── LICENSE-MIT
├── LICENSE-APACHE
└── entangled-core/ # Rust core implementation
├── Cargo.toml
├── README.md
├── src/
└── tests/
```
协议规范本身位于单独的仓库中,
[github.com/samjanny/entangled](https://github.com/samjanny/entangled),
在下面的 [规范](#specification) 部分中引用。
## 安装
将核心包添加到 Rust 项目中:
```
[dependencies]
entangled-core = "0.10"
```
或者,在开发此仓库时:
```
[dependencies]
entangled-core = { path = "entangled-core" }
```
最低支持的 Rust 版本:`1.88`。
## 快速入门
从发布者公钥推导发布者身份短语并从短语中恢复密钥:
```
use entangled_core::crypto::{derive_pip, pip_to_pubkey, PublisherSigningKey};
let publisher = PublisherSigningKey::from_seed(&[0x42; 32]);
let publisher_pubkey = publisher.verifying_key();
let pip = derive_pip(&publisher_pubkey);
assert_eq!(pip.split_whitespace().count(), 24);
let recovered = pip_to_pubkey(&pip).unwrap();
assert_eq!(recovered, publisher_pubkey);
```
PIP 是公开的。它不是种子短语、密码、恢复密钥或私钥。它是发布者身份密钥的人可读指纹。
## 构建和签名文档
`document` 模块使用 `Unsigned*` 对应物(`UnsignedManifest`、`UnsignedContent`、`UnsignedTransaction`)和 `build_*` 函数镜像每个签名线类型。构建器验证未签名的值是否与封闭模式一致,对其进行正规化(JCS),使用角色适当的密钥对正规化有效负载进行签名,并返回签名结构及其精确的序列化线字节。清单由 `K_publisher` 签名;内容和事务文档由 `K_runtime` 签名。
内容文档由运行时密钥签名:
```
use entangled_core::crypto::RuntimeSigningKey;
use entangled_core::document::{build_content, UnsignedContent};
use entangled_core::types::blocks::Block;
use entangled_core::types::inline::{InlineElement, TextMark};
use entangled_core::types::keys::SpecVersion;
use entangled_core::types::meta::Meta;
use entangled_core::types::path::EntangledPath;
use entangled_core::types::timestamp::EntangledTimestamp;
# fn demo() -> Result<(), entangled_core::document::DocumentError> {
let runtime = RuntimeSigningKey::from_seed(&[0x01; 32]);
let unsigned = UnsignedContent {
spec_version: SpecVersion,
path: EntangledPath::try_from("/articles/first-post")
.expect("valid content path"),
meta: Meta {
title: "First post".to_owned(),
published_at: EntangledTimestamp::try_from("2026-05-07T00:00:00Z")
.expect("valid timestamp"),
},
blocks: vec![Block::Paragraph {
content: vec![InlineElement::Text {
value: "Hello, world.".to_owned(),
marks: Vec::::new(),
}],
}],
// Optional content sequence number; required only when the manifest
// declares `content_root` and the path is indexed (see the content
// index section).
seq: None,
};
let (content, wire_bytes) = build_content(&unsigned, &runtime)?;
// `wire_bytes` is the exact byte sequence to serve at `content.path`.
# let _ = (content, wire_bytes);
# Ok(())
# }
```
`build_manifest` 与此类似,它还接受当前时间以在构建时强制执行 `updated` 时钟偏移量界限。`UnsignedManifest` 包含嵌套的 `origin` 和 `canary` 块;一旦组装,签名只需一个调用:
```
use entangled_core::crypto::PublisherSigningKey;
use entangled_core::document::{build_manifest, UnsignedManifest};
use entangled_core::types::timestamp::EntangledTimestamp;
# // The full UnsignedManifest construction (origin, canary, state_policy, ...)
# // is elided here; see tests/tor/integration_full.rs for a complete value.
# fn demo(unsigned: &UnsignedManifest) -> Result<(), entangled_core::document::DocumentError> {
let publisher = PublisherSigningKey::from_seed(&[0x42; 32]);
let now = EntangledTimestamp::try_from("2026-05-07T00:00:00Z")
.expect("valid timestamp");
let (manifest, wire_bytes) = build_manifest(unsigned, &publisher, &now)?;
// `wire_bytes` is the exact byte sequence to serve at `/manifest.json`.
# let _ = (manifest, wire_bytes);
# Ok(())
# }
```
`build_transaction` 类似(由 `K_runtime` 签名,接受 `UnsignedTransaction`)。在失败时,构建器返回 `DocumentError`;其 `DocumentError::Validation(Diagnostic)` 变体携带规范性诊断,因此调用者可以匹配包含的 `Diagnostic.code`(每个规范的第 11 节的 `DiagnosticCode`)。产生的 `wire_bytes` 完全往返:将它们放回 `parse_and_verify_*`(以下)中可以重新生成签名结构。
## 安全模型概述
混沌使用三个关键角色:
| 密钥 | 角色 | 暴露配置文件 |
| --- | --- | --- |
| `K_publisher` | 长期发布者身份 | 离线;仅用于发布者仪式 |
| `K_origin` | 载体端点身份 | 在线或近在线;对于 Tor v3,洋葱服务密钥 |
| `K_runtime` | 常规文档签名 | 在线;通过清单金丝雀定期轮换 |
发布者密钥签名清单。清单授权当前原始地址和运行时密钥。内容和事务文档由运行时密钥签名。
服务器被破坏可能会暴露 `K_origin` 和 `K_runtime`,但只要 `K_publisher` 保持离线且未受损害,发布者身份就应该能够生存服务器被破坏。
## 验证管道
`entangled-core` 实现了 Entangled 客户端管道的静态验证和签名验证部分:
1. 输入字节大小检查。
2. UTF-8 和 BOM 检查。
3. 带结构限制的 JSON 解析。
4. 文档类型区分。
5. 封闭模式验证。
6. 签名验证。
7. 清单类型状态过渡到金丝雀和原始地址检查。
信任状态查找、TOFU 锚定、外部验证 PIP 状态、发布者历史持久化和客户端 UI 行为仍然是嵌入客户端的责任。
## 清单验证
清单解析返回类型状态包装器而不是裸 `Manifest`。这迫使调用者显式继续或有意退出后续验证阶段。
```
use entangled_core::document::parse_and_verify_manifest;
use entangled_core::types::{EntangledTimestamp, OnionAddress};
# fn verify_manifest_bytes(
# manifest_bytes: &[u8],
# now: &EntangledTimestamp,
# fetched_onion: &OnionAddress,
# content_index_bytes: Option<&[u8]>,
# ) -> Result<(), entangled_core::validation::Diagnostic> {
let verified = parse_and_verify_manifest(manifest_bytes, now)?;
let (manifest, canary_state, content_index) = verified
.verify_canary(now)?
.verify_origin(fetched_onion, now)?
.verify_content_index(content_index_bytes)?
.into_parts();
let runtime_pubkey = manifest.canary.runtime_pubkey;
# let _ = canary_state;
# let _ = content_index;
# let _ = runtime_pubkey;
# Ok(())
# }
```
`verify_content_index` 在清单声明 `content_root` 时强制执行第 09:116 硬失败模型:调用者必须提供 `/content_index.json` 响应体字节,这些字节与 `content_root` 进行哈希验证,并结构化验证。省略 `content_root` 的清单在此处接受 `None` 并产生 `content_index = None`。
如果调用者正在构建离线工具、一致性测试或另一个上下文,其中金丝雀/原始地址/内容索引检查有意不适用,API 提供显式退出方法,例如 `skip_canary_check`、`skip_origin_check` 和 `skip_content_index_check`。
## 金丝雀状态和过期的用户覆盖合同
`verify_canary` 返回 `ManifestCanaryChecked` 清单并将分类的 `CanaryState` 通过 `canary_state()` 暴露出来。库不根据状态操作:渲染策略存在于嵌入客户端。
规范第 08:183 是规范性必须:当观察到 `CanaryState::Expired` 时,客户端必须拒绝渲染当前内容。内容区域必须是空白或客户端生成的占位符;发布者控制的内容不得出现。
规范第 08:185 将第二个必须附加到渲染块:客户端必须提供具有以下属性的会话范围用户覆盖功能:
- 一个肯定行动的铬控制(按钮、键组合或等效功能)的语义是“接受风险并继续”;被动事件不得算作接受;
- 覆盖仅适用于当前会话中受影响的站点,不跨会话持久化,不修改金丝雀状态,也不抑制铬警告;
- 覆盖活动时,一个持久、不易取消的警告必须保持可见。
第 11 节诊断代码 `E_CANARY_EXPIRED` 在 `error` 严重性下编目(rc.23 N64;该代码在 rc.10 到 rc.22 之间为 `W_CANARY_EXPIRED` 在 `warning` 严重性下,rc.23 通过重命名和提升解决了编目-行为不匹配)。现在,编目与第 08:183 规范性必须保持一致,即阻止当前内容的渲染。第 08:185 的会话范围用户覆盖功能和第 08 的许可金丝雀模式是规范定义的宽松策略 carve-outs,与第 11:87 客户端端重新分类严重性不同。`entangled-core` 识别金丝雀,暴露 `CanaryState::Expired` 并以 `error` 严重性发出诊断。覆盖状态、铬功能以及会话范围的持久性都存在于嵌入客户端。
## 内容验证
内容文档与由验证的清单授权的运行时密钥进行验证:
```
use entangled_core::document::parse_and_verify_content;
use entangled_core::types::RuntimePubkey;
# fn verify_content_bytes(
# content_bytes: &[u8],
# runtime_pubkey: &RuntimePubkey,
# ) -> Result<(), entangled_core::validation::Diagnostic> {
let content = parse_and_verify_content(content_bytes, runtime_pubkey)?;
// Higher-level clients should also bind `content.path` to the path that was fetched.
# let _ = content;
# Ok(())
# }
```
## 事务验证
事务文档也由运行时密钥签名:
```
use entangled_core::document::parse_and_verify_transaction;
use entangled_core::types::RuntimePubkey;
# fn verify_transaction_bytes(
# transaction_bytes: &[u8],
# runtime_pubkey: &RuntimePubkey,
# ) -> Result<(), entangled_core::validation::Diagnostic> {
let transaction = parse_and_verify_transaction(transaction_bytes, runtime_pubkey)?;
// Higher-level clients should bind `transaction.in_response_to` to the submit path.
# let _ = transaction;
# Ok(())
# }
```
## 核心模块
| 模块 | 目的 |
| --- | --- |
| `types` | 清单、内容、事务、块、链接、表单、路径、时间戳和密钥的线格式类型 |
| `canon` | JCS 正规化和签名输入构建 |
| `crypto` | Ed25519 包装器、签名助手、SHA-256 助手和 PIP 推导 |
| `validation` | 输入检查、封闭模式验证、诊断代码、金丝雀检查、状态策略检查和提交验证 |
| `document` | 高级构建器、解析器和清单类型状态包装器 |
| `state` | 客户端状态存储和提交体构建助手 |
| `tor` | Tor v3 葱地址解析、校验和验证和原始地址绑定 |
## 开发
运行测试套件:
```
cargo test --all --locked
```
运行格式化和 lint 检查:
```
cargo fmt --all --check
cargo clippy --all-targets --all-features -- -D warnings
```
如果已安装 `cargo-deny`,则运行依赖项/许可证/建议检查:
```
cargo deny check advisories licenses bans sources
```
推荐的本地预发布检查是:
```
cargo fmt --all --check
cargo test --all --locked
cargo clippy --all-targets --all-features -- -D warnings
cargo deny check advisories licenses bans sources
```
## 安全态势
核心包禁止在包根目录中使用不安全 Rust:
```
#![forbid(unsafe_code)]
```
一些传递依赖项可能内部使用 `unsafe` 进行加密数学或 SIMD 优化。这些是依赖级别实现细节,不是此包中的不安全代码。
与安全相关的设计选择包括:
- 严格的 Ed25519 验证;
- 每个文档家族的单独签名域;
- 限制输入大小和结构验证;
- JSON 解析期间的重复键拒绝;
- 带未知字段拒绝的封闭模式;
- 签名和验证之前的确定性正规化;
- 明确的 Tor v3 原始地址绑定;
- 基于PIP的发布者身份模型;
- 通过清单金丝雀运行时密钥轮换。
如果您报告安全漏洞,请包括:
- 受影响的包/版本或提交;
- 如果有的话,最小重现程序;
- 该问题是否影响签名验证、正规化、解析、状态处理、原始地址绑定或 API 故意使用。
## 规范
协议规范位于单独的仓库中:
[github.com/samjanny/entangled](https://github.com/samjanny/entangled).
- [`00-overview.md`](https://github.com/samjanny/entangled/blob/main/specs/00-overview.md)
- [`01-glossary.md`](https://github.com/samjanny/entangled/blob/main/specs/01-glossary.md)
- [`02-document-schema.md`](https://github.com/samjanny/entangled/blob/main/specs/02-document-schema.md)
- [`03-block-types.md`](https://github.com/samjanny/entangled/blob/main/specs/03-block-types.md)
- [`04-canonicalization.md`](https://github.com/samjanny/entangled/blob/main/specs/04-canonicalization.md)
- [`05-keys-and-signing.md`](https://github.com/samjanny/entangled/blob/main/specs/05-keys-and-signing.md)
- [`06-manifest.md`](https://github.com/samjanny/entangled/blob/main/specs/06-manifest.md)
- [`07-state.md`](https://github.com/samjanny/entangled/blob/main/specs/07-state.md)
- [`08-canary.md`](https://github.com/samjanny/entangled/blob/main/specs/08-canary.md)
- [`09-transport.md`](https://github.com/samjanny/entangled/blob/main/specs/09-transport.md)
- [`10-client-behavior.md`](https://github.com/samjanny/entangled/blob/main/specs/10-client-behavior.md)
- [`11-errors-and-versioning.md`](https://github.com/samjanny/entangled/blob/main/specs/11-errors-and-versioning.md)
有关操作指南,请参阅
[`docs/operator-playbook.md`](https://github.com/samjanny/entangled/blob/main/docs/operator-playbook.md).
## 许可证
代码在以下任一许可证下双许可:
- MIT 许可证
- Apache 许可证,版本 2.0
协议/规范文档根据规范仓库中声明的许可证单独受保护。
[`LICENSE.md`](https://github.com/samjanny/entangled/blob/main/LICENSE.md).
标签:Apache许可证, CVE, Ed25519, JSON, Rust, Tor网络, 内容分发, 加密协议, 协议实现, 协议库, 可视化界面, 安全可观测性, 客户端渲染, 开源协议, 开源框架, 持续集成, 数字签名, 文档验证, 无cookies, 无DOM脚本, 无HTML, 无JavaScript, 版本控制, 状态管理, 签名文档, 结构化数据, 网络匿名, 网络安全, 网络安全, 网络流量审计, 通知系统, 隐私保护, 隐私保护