news 2026/9/15 17:07:16

git-bug 身份(Identity)数据格式规范:从版本链、Lamport 时钟到密钥轮换的完整实现解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
git-bug 身份(Identity)数据格式规范:从版本链、Lamport 时钟到密钥轮换的完整实现解析

git-bug 身份(Identity)数据格式规范:从版本链、Lamport 时钟到密钥轮换的完整实现解析

【免费下载链接】git-bugDistributed, offline-first bug tracker embedded in git项目地址: https://gitcode.com/GitHub_Trending/gi/git-bug

导读

本文是 git-bug 分布式离线优先缺陷追踪器中**身份数据格式(Identity Format)**的正式规范与源码级解析。身份代表"操作作者"(即用户),它们独立于 bug 等实体存储,仅通过 ID 被 operation pack 引用。读完本文,你将掌握 git-bug 身份如何以 git commit 线性链形式存储、version blob 的 JSON 字段语义、SHA-256 身份 ID 的派生规则、仅允许 fast-forward 的合并策略,以及面向未来签名验证的密钥查找算法。文末附有 测试向量 与对应的 Go 实现路径,可帮助读者直接对照验证。

1. 概览:身份是一种"随时间变化"的可变记录

git-bug 把身份建模为用户资料随时间的可变记录,其形式化定义如下:

  • 一个身份是用户 profile(姓名、联系方式、密码学公钥)在时间轴上的线性版本序列(linear sequence ofversions);
  • 每个版本都是某个时间点上用户信息的完整快照;
  • 版本一旦写入即不可变(immutable),更新身份的唯一方式是追加一个新版本
  • 身份的当前状态由最后一个版本决定(见 identity.go 中Name()Email()Login()Keys()等均读取lastVersion())。

需要特别强调的是:身份格式与通用的 DAG 实体格式(见 dag-entity.md)完全不同

维度DAG 实体格式身份格式
链结构DAG(有向无环图),支持并发合并简单线性 commit 链,每 commit 至多一个父节点
数据载体OperationPack(操作包)单 blob(version
变更语义追加操作(operation)追加版本(version)
合并支持并发场景的确定性合并仅 fast-forward,冲突直接失败

当前格式版本:2(源码常量定义见 version.go)。

2. Git 引用(Reference)布局

身份通过以下两种 git reference 暴露:

Reference 模式含义
refs/identities/<identity-id>本地身份
refs/remotes/<remote>/identities/<identity-id>远端跟踪身份(remote-tracking identity)

其中<identity-id>是身份的 64 位小写十六进制 SHA-256 ID(见 §5 身份 ID 派生)。

源码中的常量(identity.go)与此表一一对应:

const identityRefPattern = "refs/identities/" const identityRemoteRefPattern = "refs/remotes/%s/identities/" const versionEntryName = "version"

本地身份的读写入口为ReadLocal(repo, id)(拼装refs/identities/+ ID)与ReadRemote(repo, remote, id)(拼装远端模式),见 identity.go。ListLocalIds则通过repo.ListRefs(identityRefPattern)枚举全部本地身份 ID。

3. Commit 与 Tree 结构:强制"恰好一个 version 条目"

身份线性链上的每个 commit都指向一个 git tree,该 tree必须恰好包含一个条目

Tree 条目名对象类型说明
versionblobJSON 序列化的身份版本(见 §4 Version Blob)

读取方(reader)必须拒绝以下两种情况:

  1. tree 条目数 ≠ 1;
  2. 唯一条目不叫version

源码中的读取校验逻辑(identity.go)逐条实现了该强制规则:

entries, err := repo.ReadTree(hash) ... if len(entries) != 1 { return nil, fmt.Errorf("invalid identity data at hash %s", hash) } entry := entries[0] if entry.Name != versionEntryName { return nil, fmt.Errorf("invalid identity data at hash %s", hash) } data, err := repo.ReadData(entry.Hash) // 读取 version blob ... json.NewDecoder(data).Decode(&version) // 解码 JSON 版本

写入侧同样严格:Commit()为每个未提交版本创建一个只含version条目的 tree(identity.go):

tree := []repository.TreeEntry{ {ObjectType: repository.Blob, Hash: blobHash, Name: versionEntryName}, } treeHash, err := repo.StoreTree(tree)

链是线性的:每个 commit 至多一个父节点(repo.StoreCommit(treeHash, lastCommit)把上一个版本的 commit 作为父节点,首个版本则无父节点)。两个无法 fast-forward 合并的并发编辑将产生冲突,详见 §6 合并。

4. Version Blob:身份版本的 JSON 序列化

每个versionblob 是一个 JSON 对象,字段定义如下:

字段JSON key类型必填说明
格式版本versioninteger必须等于2
姓名namestring用户的显示名
邮箱emailstring邮箱地址(来自 git config 或 bridge 导入)
登录名loginstring来自 bridge 的用户名(如 GitHub login)
头像 URLavatar_urlstring头像图片的 URL
公钥列表pub_keyskey 对象数组从本版本起有效的 PGP 公钥集合
Lamport 时钟timesobject(string→integer)本版本创建时所有已知时钟值的快照(见 §4.1)
Unix 时间戳unix_timeinteger版本创建的墙钟时间(自 epoch 起的秒数)
Noncenoncebase64 字符串随机字节(20–64 字节),用于保证首个版本 ID 的唯一性
元数据metadataobject(string→string)任意键值对

读取方必须拒绝version字段 ≠ 2 的版本。

源码侧的对应结构体(version.go)清晰展示了字段与 JSON 序列化关系——注意nameemailloginavatar_urlpub_keysmetadata均带omitempty,为空时直接省略;而versiontimesunix_timenonce总是输出:

type versionJSON struct { FormatVersion uint `json:"version"` Times map[string]lamport.Time `json:"times"` UnixTime int64 `json:"unix_time"` Name string `json:"name,omitempty"` Email string `json:"email,omitempty"` Login string `json:"login,omitempty"` AvatarUrl string `json:"avatar_url,omitempty"` Keys []*Key `json:"pub_keys,omitempty"` Nonce []byte `json:"nonce"` Metadata map[string]string `json:"metadata,omitempty"` }

版本号不符时的反序列化直接报格式错误(version.go):entity.NewErrInvalidFormat(aux.FormatVersion, formatVersion)

4.1 Lamport Times Map

times字段记录本身份版本被创建的那一刻,所有实体类型的 Lamport 时钟值。键名遵循<namespace>-create<namespace>-edit模式(例如bugs-createbugs-edit)。它的作用是让身份版本可以与其他实体进行时间排序——身份何时被修改,与其他实体(如 bug)的操作之间存在因果先后。

示例:某仓库只含bugs实体类型,其中 14 个 bug 被创建过,最近一次编辑时钟为 137:

"times": { "bugs-create": 14, "bugs-edit": 137 }

当仓库新增实体类型(如prsboards)时,它们的时钟也会出现在这里。读取方必须容忍该 map 中的未知键

源码层面,newVersion通过repo.AllClocks()一次性抓取所有已知时钟并快照(version.go):

clocks, err := repo.AllClocks() ... times := make(map[string]lamport.Time) for name, clock := range clocks { times[name] = clock.Time() }

同时Identity.Validate()(identity.go)会强制时间单调性约束:

  • 同一时钟名在新版本中的值不得倒退(non-chronological lamport clock 报错);
  • 新版本不得丢弃旧版本已有的时钟键(否则报 "version has less lamport clocks than before")。

4.2 Key 对象与密钥生命周期

[!WARNING]密钥管理尚未完全可用。实体 commit 的数据结构与签名验证逻辑已经就位,但生成、注册、管理密钥的工具链尚不完整。实践中绝大多数身份不携带任何密钥,实体 commit 也未签名。密钥格式(当前为 OpenPGP)将来可能变化。

pub_keys中的每个条目是一个 JSON 字符串,内容为ASCII-armored 的 OpenPGP 公钥(PEM 块类型PGP PUBLIC KEY BLOCK)。读取方必须忽略OpenPGP 密钥中的创建时间字段。

源码实现印证(key.go):

  • MarshalJSONarmor.Encode(&buf, openpgp.PublicKeyType, nil)将公钥序列化为 armored 文本,再包装为 JSON 字符串;
  • UnmarshalJSON反向解码并校验block.Type != openpgp.PublicKeyType时报 "invalid key type";
  • 密钥的CreationTime在往返中被显式置零(public.CreationTime = time.Time{}),与规范"必须忽略创建时间"一致。

pub_keys数组声明的是"从本版本起有效的完整密钥集合",而非相对上一版本的增量。因此:

  • 添加密钥:写一个新版本,其pub_keys包含之前所有有效密钥 + 新密钥;
  • 吊销密钥:写一个新版本,其pub_keys省略该密钥;
  • 空数组:表示从本版本起没有任何有效密钥。

该模型天然支持多密钥并存(例如每个设备一把密钥)与密钥轮换:用户可以先添加新密钥、再吊销旧密钥,避免签名能力的空窗期。

密钥在时间轴上的有效性

由于操作(operation)是在特定 Lamport 时钟时刻创作的,验证签名时应使用操作创作时有效的密钥集合,而非当前密钥集合。查找算法如下:

  1. 按时间顺序遍历身份版本;
  2. 对每个版本读取其times[<namespace>-edit]值;
  3. 返回最新的、时钟值 ≤ 该操作编辑时钟的版本的pub_keys

这意味着:被吊销的密钥对吊销之前签署的操作仍然有效(不会让历史签名失效)。

源码实现为Identity.ValidKeysAtTime(clockName, time)(identity.go),逻辑与规范完全一致:

func (i *Identity) ValidKeysAtTime(clockName string, time lamport.Time) []*Key { var result []*Key var lastTime lamport.Time for _, v := range i.versions { refTime, ok := v.times[clockName] if !ok { refTime = lastTime } lastTime = refTime if refTime > time { return result // 已超出目标时钟,返回此前收集的密钥集 } result = v.keys } return result }

该接口同时被声明在 interface.go 中,供实体签名验证环节按需调用。

身份保护(尚未实现)

设计的预期方案是:一旦身份声明了至少一把密钥,其后追加到链上的新版本必须由当前有效密钥之一签名——从而阻止"拥有仓库写权限的攻击者静默添加恶意密钥或替换密钥集合"。目前该机制未实现:身份版本 commit 当前未签名,读取时也不做此类校验。

源码中IsProtected()目前恒返回false(identity.go),注释明确写着 "Todo"。

4.3 Metadata:桥接(Bridge)元数据

metadata字段是一个任意的字符串键值存储,主要由各 bridge 用来记录用户在远端平台的身份信息。键名遵循约定<bridge-name>-<field>

内置 bridge 写入的已知键:

由谁写入说明
github-loginGitHub bridge用户的 GitHub 用户名
gitlab-loginGitLab bridge用户的 GitLab 用户名
jira-loginJira bridge用户的 Jira 用户名
jira-userJira bridge用户的 Jira 用户键(Jira 内部标识)
launchpad-loginLaunchpad bridge用户的 Launchpad 用户名

第三方若添加新的 bridge 专属键,应以自己的 bridge 名作前缀以避免冲突。

源码侧,版本级元数据通过version.SetMetadata/GetMetadata/AllMetadata管理(version.go),身份级聚合则在 identity.go 中实现:

  • SetMetadata:若最后版本已提交(commitHash != ""),会先克隆追加一个新版本再写元数据(已提交数据不可变);
  • ImmutableMetadata:跨版本累积,首个定义的值优先(first defined takes precedence);
  • MutableMetadata:跨版本累积,最后定义的值优先(last defined takes precedence)。

4.4 示例 Version Blob

最小版本(无密钥、无 bridge 字段):

{ "version": 2, "times": {"bugs-create": 3, "bugs-edit": 7}, "unix_time": 1609459200, "name": "Alice", "email": "alice@example.com", "nonce": "rv5N8TwqB3sGKhBxVoNFPw==" }

带公钥和 bridge 登录名的版本

{ "version": 2, "times": {"bugs-create": 5, "bugs-edit": 12}, "unix_time": 1612137600, "name": "Alice", "email": "alice@example.com", "login": "alice-gh", "pub_keys": [ "-----BEGIN PGP PUBLIC KEY BLOCK-----\n...\n-----END PGP PUBLIC KEY BLOCK-----\n" ], "nonce": "9k3mP1QrX8aLuYoW5NcTzA==" }

读取方必须把缺失的可选字段视为空/零值loginavatar_urlpub_keysmetadata在空值时从 JSON 中省略(omitempty,见上文versionJSON结构体定义)。

关于 nonce 的补充约束来自 version.go 与Validate()(version.go):长度必须> 20 且 < 64 字节(实际生成时使用makeNonce(20),即 20 字节随机数),其作用是给"首个版本"的数据注入足够熵,保证 ID 的随机唯一性——它没有其他功能用途,读取时应忽略。

version.Validate()还包含这些写入前必须满足的约束(version.go):

  • namelogin至少设置一个("either name or login should be set");
  • nameloginemail必须为安全单行文本(text.SafeOneLine);
  • avatar_url非空时必须是合法 URL(text.ValidUrl);
  • 每个 key 必须通过Key.Validate()(公钥非空、且具备CanSign()能力,见 key.go)。

5. 身份 ID 派生(Identity ID Derivation)

身份的 ID 是第一个版本的 JSON blob 的精确字节的 SHA-256 哈希:

identity-id = hex(sha256(first_version_blob_bytes))

读取方必须校验:git reference 中编码的 ID 与由第一个版本 blob 推导出的 ID 必须一致,不一致则必须拒绝该身份。

源码中两层校验逐级落实:

  1. 通用派生函数(entity/id.go)——DeriveId对任意序列化字节计算 SHA-256 并转为 64 位小写十六进制:
    func DeriveId(data []byte) Id { sum := sha256.Sum256(data) return Id(fmt.Sprintf("%x", sum)) }
  2. 版本级计算(version.go)——version.Id()在未设置时预测序列化字节并调用DeriveIdWrite()在真正落盘写入 blob 后再次用实际字节设置 ID。由于"预测"与"写入"走的是同一段序列化代码,两者必然一致。
  3. 身份级核对(identity.go)——读取完成后比对:
    if id != i.versions[0].Id() { return nil, fmt.Errorf("identity ID doesn't math the first version ID") }

通用 ID 派生规则可参考 dag-entity.md §7。另外注意 entity/id.go 中Validate()的一个特殊分支:40 位长度的 ID 会被识别为旧版仓库格式,提示使用迁移工具升级(这与仓库格式版本演进相关,但不在本文身份规范范围内)。

6. 合并(Merge):Fast-Forward Only

身份的合并策略仅允许 fast-forward

当拉取(fetch)一个远端身份时:

  • 若远端 tip 是本地 tip 的祖先(或二者相同):不采取任何操作
  • 若本地 tip 是远端 tip 的祖先:本地引用前进到远端 tip(即 fast-forward);
  • 若两端互不为祖先(发生并发编辑):合并失败并返回错误,冲突必须人工解决。

该策略的设计理由是:身份应受单一用户控制。两个独立仓库对同一身份的并发编辑属于异常情况,静默合并可能导致密钥集合不一致(例如丢失吊销或混入未经同意的密钥)。

源码中的实现与设计注释(identity.go):

// To make sure that an Identity history can't be altered, a strict fast-forward // only policy is applied here. ... if i.versions[j].commitHash != otherVersion.commitHash { return false, ErrNonFastForwardMerge }

错误类型为ErrNonFastForwardMerge(identity.go)。函数头部的大段注释还记录了一个被否决的替代方案(基于 Lamport 时间的确定性 rebase),其否决原因是:在密钥被攻破的场景下,攻击者可以伪造带虚假 Lamport 时间的新版本插入到合法版本之前,从而劫持身份——因此最终选择了严格的 fast-forward 策略。

配合identity_actions.go提供的同步原语(identity_actions.go),完整的远端同步流程是:

  • Fetchrepo.FetchRefs(remote, Namespace)拉取远端引用,不改动本地状态;
  • MergeAll:枚举所有refs/remotes/<remote>/identities/引用,逐个校验 ID、读取远端身份、校验数据,然后按"本地不存在则直接复制引用(MergeNewStatus)/ 存在则尝试 Merge(MergeUpdatedStatus 或 MergeNothingStatus)/ 失败则 MergeInvalidStatus"处理;
  • Pull:Fetch + MergeAll,任一合并失败即返回错误;
  • Pushrepo.PushRefs(remote, Namespace)推送本地变更;
  • Remove/RemoveAll:删除本地及所有远端跟踪引用(幂等)。

7. 签名验证时的密钥查找(Key Lookup)

要验证某个实体包(entity pack)的 commit 签名,读取方需查找作者的 identity,并确定该包编辑时钟时刻有效的那组密钥:

  1. 按顺序读取作者的身份版本;
  2. 找到times[<namespace>-edit]值 ≤ 该包编辑时钟的最新版本;
  3. 该版本的pub_keys即验证用的有效密钥集合。

这与 §4.2 的密钥时间有效性算法 是同一逻辑的两种表述(一个是按身份版本查询,一个是按操作时刻查询),对应实现均为Identity.ValidKeysAtTime(clockName string, time lamport.Time) []*Key(identity.go,interface.go)。

补充说明:SigningKey(repo)(identity.go)负责在需要签发时,从当前有效密钥中挑选一把能在 keyring(repository/keyring.go)中找到对应私钥的密钥;私钥的存储与加载实现见 key.go(storePrivate写入 armored 私钥,loadPrivateKeyIdString()从 keyring 读取)。由于 §4.2 的警告 所述的工具链尚未完备,实践中签名路径很少被触发。

8. 已知局限(Known Limitations)

  • 仓库本地身份(Repository-local identities):身份 ID 由内容派生,而非来自全局注册表,因此同一个人在不同仓库中可能拥有不同身份;目前没有内置机制来断言跨仓库身份等价。
  • 不支持并发编辑(No concurrent edit support):fast-forward-only 合并策略意味着,如果同一身份在两个仓库中未先同步就各自编辑,其中一次编辑会被拒绝。在多台机器上编辑身份的用户,应在编辑前先同步

这两条局限的根源都可追溯到 §6 的合并策略 与 §5 的 ID 派生:前者决定了并发冲突必然失败,后者决定了 ID 天然绑定到具体仓库的字节内容。

9. 测试向量(Test Vectors)

本数据层的测试向量位于testdata/identity.json,涵盖三类用例:

  • id_derivation:验证"身份 ID = 首个版本 blob 精确字节的 SHA-256";expected_id由参考实现(Go 的encoding/json+crypto/sha256)计算,可通过go test ./doc/spec/... -run TestVectors生成校验;
  • tree_entries:验证任意身份 commit 的 tree 结构——恰好一个名为version的 blob 条目,无时钟条目、无额外子树;
  • version_examples:最小合法版本与带公钥版本的 JSON 示例,并注释了omitempty行为与"OpenPGP 创建时间置零并忽略"的要求。

如需了解身份格式在整个数据模型中的定位(为什么用操作而非快照、为什么用 Lamport 时钟、为什么用 git 对象),可阅读 数据模型设计文档;该层与其他实体共用层的差异对照见 格式规范总览。

参考文件索引

  • 规范正文:doc/spec/identity.md
  • 身份核心实现:entities/identity/identity.go、entities/identity/version.go、entities/identity/key.go
  • 身份接口与桩实现:entities/identity/interface.go、entities/identity/identity_stub.go
  • 远端同步动作:entities/identity/identity_actions.go
  • ID 派生通用实现:entity/id.go
  • 测试向量:doc/spec/testdata/identity.json
  • 相关规范:doc/spec/dag-entity.md、doc/spec/README.md

【免费下载链接】git-bugDistributed, offline-first bug tracker embedded in git项目地址: https://gitcode.com/GitHub_Trending/gi/git-bug

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 17:06:59

Oracle EBS总账模块全解析:从科目表到年终结账实战指南

做过几年Oracle EBS财务模块实施和运维的朋友&#xff0c;应该都有这种感觉&#xff1a;十个项目里&#xff0c;有八个的难点不在应收应付&#xff0c;而在总账&#xff08;General Ledger&#xff09;。应收付、固定资产、库存、采购&#xff0c;说白了都是业务单据的流水账&a…

作者头像 李华
网站建设 2026/9/15 17:06:40

基于联盟链的文档交易系统:智能合约与版权确权实战

简介&#xff1a;这套基于区块链的文档交易系统的毕业设计资料包&#xff0c;面向计算机相关专业的学生、教师及开发者&#xff0c;完整涵盖项目源码、详细设计文档与配套说明&#xff0c;既可用于毕业设计、课程设计演示&#xff0c;也适合作为区块链应用开发的学习范例。压缩…

作者头像 李华
网站建设 2026/9/15 17:05:15

MySQL 8.0认证插件与Navicat兼容问题详解:从1251错误到平滑升级

先分享一个真实场景&#xff1a;你费了半天劲把 MySQL 8.0 部署完&#xff0c;打开 Navicat 11 输完密码&#xff0c;结果对方甩回来一行英文&#xff1a;Client does not support authentication protocol requested by server; consider upgrading MySQL client。第一反应查密…

作者头像 李华
网站建设 2026/9/15 17:04:04

P2P人群计数论文复现:点对点回归与视角引导实战指南

写这篇博文之前我翻了翻GitHub的提交记录&#xff0c;又把自己跑实验时的终端日志翻出来核对了一遍。crowdcountingp2p这个项目&#xff0c;准确的论文名是《Crowd Counting via Perspective-Guided Point-to-Point Regression》&#xff0c;CVPR 2024的一篇工作&#xff0c;代…

作者头像 李华
网站建设 2026/9/15 17:03:42

Foundry测试框架入门:用纯Solidity写合约测试与模糊测试

开门见山说一个观察&#xff1a;如果你最近在 GitHub 上翻 Solidity 项目&#xff0c;会发现一个非常明显的趋势——越来越多的仓库把测试目录从.js/.ts文件换成了.t.sol后缀的纯 Solidity 文件&#xff0c;CI 里跑测试的命令也从npx hardhat test变成了forge test。这就是 Fou…

作者头像 李华