ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

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

2026/9/15 17:07:23 拓冰建站 浏览量
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操作包单 blobversion变更语义追加操作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必须拒绝以下两种情况tree 条目数 ≠ 1唯一条目不叫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条目的 treeidentity.gotree : []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 时钟timesobjectstring→integer是本版本创建时所有已知时钟值的快照见 §4.1Unix 时间戳unix_timeinteger是版本创建的墙钟时间自 epoch 起的秒数Noncenoncebase64 字符串是随机字节20–64 字节用于保证首个版本 ID 的唯一性元数据metadataobjectstring→string否任意键值对读取方必须拒绝version字段 ≠ 2 的版本。源码侧的对应结构体version.go清晰展示了字段与 JSON 序列化关系——注意name、email、login、avatar_url、pub_keys、metadata均带omitempty为空时直接省略而version、times、unix_time、nonce总是输出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.goentity.NewErrInvalidFormat(aux.FormatVersion, formatVersion)。4.1 Lamport Times Maptimes字段记录本身份版本被创建的那一刻所有实体类型的 Lamport 时钟值。键名遵循namespace-create与namespace-edit模式例如bugs-create、bugs-edit。它的作用是让身份版本可以与其他实体进行时间排序——身份何时被修改与其他实体如 bug的操作之间存在因果先后。示例某仓库只含bugs实体类型其中 14 个 bug 被创建过最近一次编辑时钟为 137times: { bugs-create: 14, bugs-edit: 137 }当仓库新增实体类型如prs、boards时它们的时钟也会出现在这里。读取方必须容忍该 map 中的未知键。源码层面newVersion通过repo.AllClocks()一次性抓取所有已知时钟并快照version.goclocks, 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.goMarshalJSON用armor.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 时钟时刻创作的验证签名时应使用操作创作时有效的密钥集合而非当前密钥集合。查找算法如下按时间顺序遍历身份版本对每个版本读取其times[namespace-edit]值返回最新的、时钟值 ≤ 该操作编辑时钟的版本的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()目前恒返回falseidentity.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 precedenceMutableMetadata跨版本累积最后定义的值优先last defined takes precedence。4.4 示例 Version Blob最小版本无密钥、无 bridge 字段{ version: 2, times: {bugs-create: 3, bugs-edit: 7}, unix_time: 1609459200, name: Alice, email: aliceexample.com, nonce: rv5N8TwqB3sGKhBxVoNFPw }带公钥和 bridge 登录名的版本{ version: 2, times: {bugs-create: 5, bugs-edit: 12}, unix_time: 1612137600, name: Alice, email: aliceexample.com, login: alice-gh, pub_keys: [ -----BEGIN PGP PUBLIC KEY BLOCK-----\n...\n-----END PGP PUBLIC KEY BLOCK-----\n ], nonce: 9k3mP1QrX8aLuYoW5NcTzA }读取方必须把缺失的可选字段视为空/零值。login、avatar_url、pub_keys、metadata在空值时从 JSON 中省略omitempty见上文versionJSON结构体定义。关于 nonce 的补充约束来自 version.go 与Validate()version.go长度必须 20 且 64 字节实际生成时使用makeNonce(20)即 20 字节随机数其作用是给首个版本的数据注入足够熵保证 ID 的随机唯一性——它没有其他功能用途读取时应忽略。version.Validate()还包含这些写入前必须满足的约束version.goname与login至少设置一个either name or login should be setname、login、email必须为安全单行文本text.SafeOneLineavatar_url非空时必须是合法 URLtext.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 必须一致不一致则必须拒绝该身份。源码中两层校验逐级落实通用派生函数entity/id.go——DeriveId对任意序列化字节计算 SHA-256 并转为 64 位小写十六进制func DeriveId(data []byte) Id { sum : sha256.Sum256(data) return Id(fmt.Sprintf(%x, sum)) }版本级计算version.go——version.Id()在未设置时预测序列化字节并调用DeriveIdWrite()在真正落盘写入 blob 后再次用实际字节设置 ID。由于预测与写入走的是同一段序列化代码两者必然一致。身份级核对identity.go——读取完成后比对if id ! i.versions[0].Id() { return nil, fmt.Errorf(identity ID doesnt math the first version ID) }通用 ID 派生规则可参考 dag-entity.md §7。另外注意 entity/id.go 中Validate()的一个特殊分支40 位长度的 ID 会被识别为旧版仓库格式提示使用迁移工具升级这与仓库格式版本演进相关但不在本文身份规范范围内。6. 合并MergeFast-Forward Only身份的合并策略仅允许 fast-forward。当拉取fetch一个远端身份时若远端 tip 是本地 tip 的祖先或二者相同不采取任何操作若本地 tip 是远端 tip 的祖先本地引用前进到远端 tip即 fast-forward若两端互不为祖先发生并发编辑合并失败并返回错误冲突必须人工解决。该策略的设计理由是身份应受单一用户控制。两个独立仓库对同一身份的并发编辑属于异常情况静默合并可能导致密钥集合不一致例如丢失吊销或混入未经同意的密钥。源码中的实现与设计注释identity.go// To make sure that an Identity history cant be altered, a strict fast-forward // only policy is applied here. ... if i.versions[j].commitHash ! otherVersion.commitHash { return false, ErrNonFastForwardMerge }错误类型为ErrNonFastForwardMergeidentity.go。函数头部的大段注释还记录了一个被否决的替代方案基于 Lamport 时间的确定性 rebase其否决原因是在密钥被攻破的场景下攻击者可以伪造带虚假 Lamport 时间的新版本插入到合法版本之前从而劫持身份——因此最终选择了严格的 fast-forward 策略。配合identity_actions.go提供的同步原语identity_actions.go完整的远端同步流程是Fetchrepo.FetchRefs(remote, Namespace)拉取远端引用不改动本地状态MergeAll枚举所有refs/remotes/remote/identities/引用逐个校验 ID、读取远端身份、校验数据然后按本地不存在则直接复制引用MergeNewStatus/ 存在则尝试 MergeMergeUpdatedStatus 或 MergeNothingStatus/ 失败则 MergeInvalidStatus处理PullFetch MergeAll任一合并失败即返回错误Pushrepo.PushRefs(remote, Namespace)推送本地变更Remove/RemoveAll删除本地及所有远端跟踪引用幂等。7. 签名验证时的密钥查找Key Lookup要验证某个实体包entity pack的 commit 签名读取方需查找作者的 identity并确定该包编辑时钟时刻有效的那组密钥按顺序读取作者的身份版本找到times[namespace-edit]值 ≤ 该包编辑时钟的最新版本该版本的pub_keys即验证用的有效密钥集合。这与 §4.2 的密钥时间有效性算法 是同一逻辑的两种表述一个是按身份版本查询一个是按操作时刻查询对应实现均为Identity.ValidKeysAtTime(clockName string, time lamport.Time) []*Keyidentity.gointerface.go。补充说明SigningKey(repo)identity.go负责在需要签发时从当前有效密钥中挑选一把能在 keyringrepository/keyring.go中找到对应私钥的密钥私钥的存储与加载实现见 key.gostorePrivate写入 armored 私钥loadPrivate按KeyIdString()从 keyring 读取。由于 §4.2 的警告 所述的工具链尚未完备实践中签名路径很少被触发。8. 已知局限Known Limitations仓库本地身份Repository-local identities身份 ID 由内容派生而非来自全局注册表因此同一个人在不同仓库中可能拥有不同身份目前没有内置机制来断言跨仓库身份等价。不支持并发编辑No concurrent edit supportfast-forward-only 合并策略意味着如果同一身份在两个仓库中未先同步就各自编辑其中一次编辑会被拒绝。在多台机器上编辑身份的用户应在编辑前先同步。这两条局限的根源都可追溯到 §6 的合并策略 与 §5 的 ID 派生前者决定了并发冲突必然失败后者决定了 ID 天然绑定到具体仓库的字节内容。9. 测试向量Test Vectors本数据层的测试向量位于testdata/identity.json涵盖三类用例id_derivation验证身份 ID 首个版本 blob 精确字节的 SHA-256expected_id由参考实现Go 的encoding/jsoncrypto/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.goID 派生通用实现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),仅供参考