# Pijul × AT Protocol:DID 作为一等公民的身份设计 ## 动机 Git 在 Tangled 中的身份是事后关联的:commit 里的 `author.email` 是自声明字符串,平台通过维护 `committerDid` 字段来补救,但这个关联只存在于平台数据库,没有密码学保证。 Pijul 提供了重新设计的机会。Pijul change 的 `[[authors]]` 字段天然是结构化 KV map,可以直接承载 DID。更重要的是,AT Protocol 本身就有完整的签名基础设施——用户的 PDS 会对其 AT repo 的每次写入做 MST 签名,使用的正是 DID 的原生签名密钥。 因此,**不需要为 pijul 另造一套密钥体系**。DID 即身份,PDS 上的 AT record 即签名证明。 --- ## 设计原则 1. **DID 是唯一身份标识**:change 文件中只记录 `did`,不引入独立的 pijul 签名密钥 2. **签名由 PDS 完成**:推送时在用户 PDS 上创建 `sh.tangled.pijul.refUpdate` record,PDS 用 DID 的原生密钥对 AT repo 做 MST 签名,这就是归属证明 3. **change hash 不变**:DID 写入 change 的 `unhashed` 部分,不影响 hash 计算,客户端和服务器的 change 保持一致 4. **验证依赖 AT Protocol 网络**:这与"DID 本身需要联网解析"一致,不是额外代价 --- ## 签名链 **pijul push(CLI 侧发布):** ``` 用户运行 pijul identity link-at → 本地 keyring 存入 AT access token ↓ pijul push → knot server 接收 changes ↓ CLI 从 keyring 取 token,以用户身份向其 PDS 发 createRecord 请求 ↓ PDS 将 sh.tangled.pijul.refUpdate 写入用户的 AT repo ↓ PDS 用 DID 原生签名密钥对 MST 做签名 ↓ 结果:该 DID 密码学地声明了这些 change hash 归属于自己 ``` **apply/merge(appview 侧发布):** ``` 用户通过 OAuth 登录 appview → appview 持有用户 AT 会话 ↓ 用户 merge discussion → appview 以用户身份向其 PDS 发 createRecord 请求 ↓ (同上)PDS MST 签名完成 ``` 与独立 pijul 签名密钥方案的对比: | | 独立 pijul 密钥方案 | DID-only 方案 | |---|---|---| | 签名密钥 | 单独生成的 Ed25519 | DID 原生签名密钥 | | 信任链 | pijul key → prove → DID(两跳) | DID(直接) | | 签名位置 | change 文件内 | PDS 的 AT repo MST | | 需要 prove 流程 | 是 | 否 | | 服务器可否伪造 | 否(无用户私钥) | 否(需用户持有 AT 凭证) | DID-only 方案的签名不可伪造:createRecord 请求需用户的有效 AT Protocol 会话(push 时由 CLI 持有,merge 时由 appview 代持),这个会话本身就是用户主动授权的结果。 --- ## Change 文件格式 DID 写入 change 的 **hashed `[[authors]]`** 部分,参与 hash 计算: ```toml # change 文件(hashed 部分,决定 change identity) message = 'Fix bug in parser' timestamp = '2026-04-03T10:00:00Z' [[authors]] did = "did:plc:xxxxxxxxxxxx" # DID 是唯一身份,无 key 字段 # change 文件(unhashed 部分) # signature = "..." # pijul 私钥对 hash 的签名(现有机制,独立于 DID) ``` 配置了 DID 的 identity 在 record 时只写 `did`,不写 `key`。未配置 DID 时保持原有 `key` 行为,向后兼容非 Tangled 服务器。hash 包含 DID,PDS record 里列出的 change hash 密码学绑定了 DID 与内容。 --- ## Lexicon 设计 ### `sh.tangled.pijul.refUpdate`(更新现有) 现有 lexicon 缺少 `committerDid` 和正确的 `repo` 格式,更新如下: ```json { "lexicon": 1, "id": "sh.tangled.pijul.refUpdate", "defs": { "main": { "type": "record", "description": "Published to the committer's PDS when pijul changes are pushed. The PDS MST signature over this record is the verifiable proof of authorship.", "key": "tid", "record": { "type": "object", "required": ["repo", "channel", "newState", "changes", "committerDid"], "properties": { "repo": { "type": "string", "format": "at-uri", "description": "AT URI of the repository (at://did:plc:owner/sh.tangled.repo/name)" }, "channel": { "type": "string", "description": "Channel that was updated" }, "oldState": { "type": "string", "description": "Pijul Merkle state hash before push (empty for new channels)" }, "newState": { "type": "string", "description": "Pijul Merkle state hash after push" }, "changes": { "type": "array", "items": { "type": "string" }, "description": "Base32 change hashes pushed in this operation, in application order" }, "committerDid": { "type": "string", "format": "did", "description": "DID of the pusher. Redundant when this record is in the committer's own PDS, but preserved for when it is replicated elsewhere." }, "languages": { "type": "object", "description": "Optional map of language name to line count, computed by knot server" } } } } } } ``` **关键决策**:此 record 发布到**执行操作者**的 PDS,语义是"我(操作者)将这些 change 推送/合并进了这个 repo"。 - 直接推送(`pijul push`):操作者 = 推送者,record 发到推送者的 PDS - apply/merge:操作者 = 执行 merge 的人(通常是 repo owner),record 发到他的 PDS 这样 appview 始终持有操作者的 AT Protocol 会话,可以直接调用其 PDS。`committerDid` 字段是执行操作者的 DID。原始 change 创作者的身份保留在 change 文件的 `unhashed.identity.did` 中,两者独立。 --- ## 推送流程 ### pijul push(HTTP 或 SSH,CLI 认证) Pijul push 的 PDS 发布由 **CLI 本地完成**,无需服务器中转。用户先通过 `pijul identity link-at` 将 AT Protocol 凭证与本地 identity 绑定: ``` pijul identity link-at --pds https://bsky.social → 提示输入 handle + app password → 认证成功后:DID + at_pds 写入 identity config,access token 存入 keyring ``` push 流程: ``` 用户: pijul push https://tangled.sh/did:plc:xxx/myrepo ↓ knot server: 1. 接收 change 文件,写入 repo 2. (TODO)将 DID 写入每个 change 的 unhashed.identity.did (需 Bearer token 认证,knot server 从 token 解析 DID) CLI(push 成功后): 3. 从 keyring 取 AT access token(link-at 时存入) 4. 解析 remote URL 中的 DID,构造 at:// repo URI 5. 调用 com.atproto.repo.createRecord → 发布到用户 PDS 6. PDS MST 签名完成 → 归属证明建立 ``` 注意:remote URL 须使用 DID 格式(`https://tangled.sh/did:plc:xxx/repo`),不能用 handle, 否则 CLI 无法直接推导 AT URI。未配置 `link-at` 时 push 照常工作,只是不发布 PDS record。 SSH push 与 HTTP push 共用同一套 CLI 发布逻辑(均经过 `Push::run()` → `remote.finish()`)。 ### apply/merge 操作 merge 由 appview 的 `discussions.go` 处理(pijul-only)。appview 已持有执行者的 AT 会话, 可直接内联发布,无需 event queue 中转: ``` Bob merge 了 Alice 的 change: appview: actorDid = Bob 的 DID(执行操作者) appview: 用 Bob 的 OAuth 会话调用 com.atproto.repo.createRecord committerDid = Bob 的 DID,record 发布到 Bob 的 PDS ``` Bob 的 PDS 记录了他将 Alice 的 change 合并进了这个 repo。Alice 的创作身份保留在 change 文件的 `unhashed.identity.did` 中。 --- ## 验证流程 ### 验证 change X 的推送记录 ``` 1. 查询 knot server 的 changes 历史,得到 committerDid(谁推送/合并了这个 change) 2. 查询 committerDid 的 PDS,列举 sh.tangled.pijul.refUpdate 记录, 找到 changes[] 包含此 change hash 的那条 3. PDS 的 MST 签名(验证方法:AT Protocol 标准 repo 验证流程) 证明:该 DID 声明将此 change hash 推送/合并进了该 repo ``` ### 验证 change X 的创作身份 ``` 1. 打开 change 文件,读取 unhashed.identity.did(原始作者) 2. 该 DID 是 change 创作者的自声明身份 (无密码学签名,但 change hash 不变,内容完整性有保证) ``` ### 查询某 DID 的所有推送/合并历史 ``` GET /xrpc/com.atproto.repo.listRecords ?collection=sh.tangled.pijul.refUpdate → 按 repo 字段分组,得到该 DID 在所有 repo 的操作历史 ``` 这是一个**去中心化的操作图**:不依赖任何单一平台数据库,任何人都可以从 AT Protocol 网络重建完整的推送记录。 --- ## 与 fedi-xanadu 的集成 Fedi-xanadu 是本设计的第一个实现者,使用场景略有不同——用户通过 Web 界面编辑文章,服务器代为 record。 流程: ``` 用户通过 Web UI 编辑并保存文章 → fx-server 的 publish_article_content 调用 pijul.record(node_id, message, Some(&user.did)) → pijul change 的 unhashed 写入 user.did → fx-atproto 在用户 PDS 上发布 sh.tangled.pijul.refUpdate → 文章历史页面展示 DID,链接到用户主页 ``` 由于 fedi-xanadu 已有用户的 AT Protocol 会话,向其 PDS 写 record 不需要额外的认证步骤。 --- ## 开放问题 **1. 历史 change 的追溯** 对于在本方案实施之前推送的 change,可以允许用户发布一条声明 record 来认领历史 change,由 knot server 验证用户确实有该 repo 的推送记录后接受。 **2. `[[authors]]` 格式与 pijul 上游的协调** `did` 字段写入 hashed `[[authors]]` 目前是 Tangled 的约定。应与 pijul 上游协调是否纳入官方格式,或在文档中明确为扩展字段。