身份与 KEL
每个 ANet 节点都以一个身份运行。本页说明该身份是什么、如何创建并绑定到 Hub、其密钥如何随时间更换,以及哪些操作依赖恢复口令。内容涵盖作用于身份的命令(anet identity 与 anet hub-register)以及身份可能处于的状态。
身份是什么
身份由身份标识(Agent identifier,AID)、密钥事件日志(key event log,KEL)以及该日志当前指明的私钥组成。
- AID
其他节点称呼该 Agent 所用的名称。AID 是 KEL 第一个事件的内容标识(CID),为 59 个字符的字符串,以
b开头,采用小写 base32。AID 由第一个事件导出,因此无需询问任何第三方即可对照日志核验,且密钥变更时它不会改变。- KEL
属于某个 AID 的有序密钥事件列表。事件类型有三种:
icp(inception,起始)创建身份并指明第一个签名密钥。
rot(rotation,轮换)更换签名密钥。该事件由前一个事件预先承诺的密钥签名,而不是由被替换的密钥签名。
dip(deactivation,停用)永久终止身份,此后不再接受任何事件。
每个事件都带有时间戳,且大于前一事件的时间戳。日志只有在无误重放时才被接受:事件类型、签名、承诺、时间戳和规范编码都必须有效。一份日志最多包含 256 个事件和 64 KiB。
- 私钥
最新事件所指明的签名密钥,以及 消息传递 中所述的加密密钥。签名密钥保存在节点的数据目录中。下一次轮换将要公开的密钥不保存在该处,而是由恢复种子导出(参见 恢复种子与口令)。
本文档通篇使用的示例 AID 为占位值:
bafyreiaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
在表格中输出 AID 的命令会将其缩短,例如 …7hq2abcd。以 AID 为参数的命令则要求完整的字符串。
身份的存放位置
默认节点的身份存放在 ~/.anet 中。环境变量 ANET_DATA_DIR 可选择其他目录。同一台机器上的其他身份通过 anet id new <name> 创建,存放在 ~/.anet/ids/<name> 中。KEL 是数据目录中的 identity.kel 文件。由 ANet 0.2 写入的数据目录不会被 0.3.0 读取;守护进程拒绝以其启动并报告 identity_v02。身份不做迁移,须重新创建。
$ anet id new coder
$ anet id ls
$ anet --id coder status
尚未加入 Hub 的节点,其身份仅存在于该机器上。其他节点无法验证它,节点在加入 Hub 之前也无法收发消息(参见 见证与密钥确认)。
见证与密钥确认
仅凭 KEL 本身,收件方无法判断其最新事件是否仍是最新的。已被轮换掉的密钥,对照该密钥使用期间有效的那一段日志,仍能通过验证。ANet 通过见证(witness)解决这一问题。
Agent 注册所在的 Hub 成为其 KEL 的见证。这一点记录在日志本身中:第一个 rot 事件将该 Hub 的 AID 指明为见证。见证 Hub 随后签发一份短期有效的签名声明,指明日志中的最新事件。该声明称为密钥状态通知(key state notice,缩写 KSN),在面向用户的文字中称为密钥确认。
KSN 具有以下性质:
它声明 AID、最新事件的序号和标识、见证 Hub,以及 Hub 观察到该状态的时间。
发送方将其当前 KSN 连同 KEL 放入每个密封信封内,只有收件方可以读取。只有当 KSN 与信封中 KEL 的最新事件一致,并且观察时间距今不超过六小时,收件方才接受该消息。
该通知是公开信息,定期刷新,也可从 Hub 的注册表获取。
唯一的例外是预览帧,它不携带通知(参见 预览流)。
对于已轮换掉的密钥的持有者,其后果是有界的。在通知可接受的时效内,该持有者仍可向尚未得知轮换的节点发送消息。计划轮换之后,已知新密钥历史的节点再接受旧密钥一小时。紧急轮换或停用之后,该节点立即拒绝旧密钥。这些界限列于 保护的局限。
节点无需运营人员操作即可保持自身通知的有效。通知的时效决定 身份状态 所述的身份状态。到三小时时,状态会附带警告;到 5 小时 50 分钟时,节点停止发出消息,并将其排队,直至新通知到达。
见证在注册时选定,并固定在身份中。它只能通过密钥轮换更改,即以另一个 Hub 执行 anet hub-register 所完成的操作(参见 更换 Hub)。一份 KEL 恰好指明一个见证,0.3.0 不支持多个见证。
恢复种子与口令
下一次轮换的签名密钥不得被读取数据目录的人获得。因此,ANet 由一个 32 字节的恢复种子导出此后的每一个密钥,并将种子加密后存放在数据目录之外,由恢复口令保护。
创建
恢复种子在节点首次加入 Hub 时创建。在终端中,anet hub-register 此时会询问口令:
$ anet hub-register https://hub.example.com --name coder
First time joining a hub: set a recovery passphrase. You will need it to rotate keys, switch hubs or recover this identity on another machine; anet does not store it, and it cannot be recovered if forgotten.
New recovery passphrase: ************
Type it again: ************
Recovery file written to ~/.anet-recovery/x2k97hq2abcd.anetrec (passphrase-encrypted). Keep a copy somewhere else.
口令与文件适用以下规则:
口令必须是有效的 UTF-8,且经 Unicode 规范化(NFC)后至少包含 12 个字符。两次输入不一致时,提示会重复出现。
口令只从控制终端读取。它从不接受命令行参数或环境变量传入,守护进程也从不读取它。
种子使用 XChaCha20-Poly1305 加密,密钥由口令经 Argon2id 导出。文件以 0600 权限写入
~/.anet-recovery/<last 12 characters of the AID>.anetrec,所在目录权限为 0700。首次创建文件时,--recovery-file F可选择其他路径。该文件单独对任何人都没有用处;与口令一起,则可以轮换、恢复或停用该身份。应在另一台机器或另一种介质上保存一份副本。
只有四类操作需要口令:轮换、更换 Hub、在另一台机器上恢复,以及停用。没有口令,这些操作都无法执行,但身份仍可正常收发。口令丢失后,唯一的办法是创建新身份(anet id new)并通知联系人。allow 名单、评价以及作为官方 Agent 的任何列示都绑定在旧 AID 上,不会延续。
无终端加入
没有终端运行的 Agent,例如由编码工具启动的 Agent,同样可以加入 Hub。此时 anet hub-register 不询问口令即完成加入。种子使用在本机生成并存放在数据目录中的密钥加密,保存在 recovery_local.key 和 recovery_local.anetrec 两个文件中。此时的身份为未保护状态:
$ anet hub-register https://hub.example.com --name coder < /dev/null
This identity has no recovery passphrase yet: if its data directory leaks, the identity can be taken over and cannot be recovered. Run anet identity protect in a terminal.
→ anet identity protect
Registered at https://hub.example.com.
未保护的身份可以正常收发。在口令设置之前,anet status、anet doctor、MCP 工具 node_status 以及本地 A2A 接口上每个任务的元数据都会显示提醒,且没有可以关闭该提醒的设置。
在设置口令之前,种子及解密种子的密钥都在数据目录中。任何获得该目录副本的人,包括以同一操作系统用户运行的程序,都可以抢先轮换该身份并永久接管。这一风险与 ANet 0.2 相同,设置口令后即告消除;参见 为未保护的身份设置口令。
命令
anet identity 与 anet id 是同一组命令。更改身份的子命令(protect、rotate、recover、deactivate)从终端读取口令,没有终端时报告 tty_required,此时不向守护进程发送任何请求。MCP 接口和本地 A2A 接口不提供更改身份的操作,它们报告相同的状态和相同的说明语句。
命令 |
用途 |
|---|---|
|
列出、创建和选择本机上的身份。 |
|
删除本机上某个身份的数据。这不会停用该身份。 |
|
显示密钥、见证和恢复文件。 |
|
为未保护的身份设置恢复口令。 |
|
更换签名密钥(计划轮换)。 |
|
在可能发生泄露后更换签名密钥。 |
|
加入 Hub;若 Hub 不同于当前见证,则为更换 Hub。 |
|
在另一台机器上恢复身份。 |
|
永久终止身份。 |
状态
anet identity status 读取守护进程;守护进程未运行时读取数据目录。加上 --json 时,它输出守护进程的应答(key_seq、state、witness、last_rotation、recovery 及相关成员),以及由命令自行计算的 recovery_file 成员。
$ anet identity status
identity coder …7hq2abcd
key number 3, last rotated 10-02 14:03 (scheduled)
witness hub https://hub.example.com, key confirmed 4 minutes ago
recovery file ~/.anet-recovery/x2k97hq2abcd.anetrec (passphrase-encrypted)
state coder …7hq2abcd, key confirmed by https://hub.example.com 4 minutes ago.
对于未保护的身份,恢复文件一行显示为 (no passphrase, kept with the data directory),提醒则输出在状态行下方。
为未保护的身份设置口令
anet identity protect 在终端中运行。它两次询问新口令,写入由口令加密的恢复文件,然后执行两次计划轮换:
$ anet identity protect
Set a recovery passphrase for coder (…7hq2abcd): it is needed to rotate keys, switch hubs or recover this identity; anet does not store it and it cannot be recovered. The unprotected recovery file in the data directory is then voided and deleted.
New recovery passphrase: ************
Type it again: ************
Recovery file written to ~/.anet-recovery/x2k97hq2abcd.anetrec (passphrase-encrypted). Keep a copy somewhere else.
Recovery passphrase set: key number 1 → 3, confirmed by https://hub.example.com; the unprotected recovery file in the data directory is deleted, and a copy taken earlier can no longer rotate or take over this identity.
需要两次轮换,是因为第一次轮换公开的密钥由旧种子导出,而数据目录的副本同样包含旧种子。只有第二次轮换公开的密钥才由新种子导出。此后,无论是当前密钥还是下一个密钥,都无法从更早的副本所含的任何内容算出。第一次轮换的密钥在计划轮换的一小时宽限期内仍然有效,在尚未得知轮换的节点处最长可有效六小时。
若命令在第一次轮换之后被中断,身份仍为未保护状态,提醒变为 Setting the recovery passphrase is one step short。再次运行命令会询问已设置的口令并完成第二次轮换。如果口令是在 Hub 准入该身份之前设置的,则不发生轮换;新种子在加入完成时使用。
protect 不会更换加密密钥或信箱密钥。如果数据目录可能在口令设置之前已被复制,应改用 anet identity rotate --emergency,它同时会设置口令。
计划轮换
计划轮换只更换签名密钥,不作其他更改。进行中的任务继续执行,其他节点在收到下一条消息时更新。
$ anet identity rotate
This rotates the signing key of coder (…7hq2abcd); others update when they get your next message, and tasks in progress are not affected.
Recovery passphrase: ************
Rotated: key number 3 → 4, confirmed by https://hub.example.com.
命令解密恢复文件,检查导出的密钥与日志中记录的承诺一致,然后请守护进程在见证 Hub 发布 rot 事件,并最多等待 60 秒让 Hub 接受该事件。计划轮换要求见证可达,否则报告 witness_unreachable。
--new-recovery 另外更换恢复种子和口令。口令可能已被他人知晓时使用。命令会轮换两次密钥,使旧种子无法再导出当前密钥或下一个密钥。新的恢复文件先以临时名称写入,待 Hub 接受该事件后才替换旧文件;发现未完成临时文件的命令会将其完成或删除。
宽限期
计划轮换之后,旧密钥在收件方处仍可接受一小时,自收件方的 Hub 接受该消息的时间起算。在轮换之前已到达信箱的消息,无论收件方何时处理都被接受。加密密钥和信箱签名密钥不被更换。
紧急轮换
紧急轮换适用于私钥或整个数据目录可能已被泄露的情形。
$ anet identity rotate --emergency
Emergency rotation of coder (…7hq2abcd): for when the private key or the data directory may have leaked. It will:
· stop the old key at once: peers that know your new key refuse messages signed with the old one right away, the rest within 6 hours
· replace your encryption keys and void every delivery credential you have issued
· end every task in progress (the other side sees that you rotated keys in an emergency)
· replace this node's control and A2A tokens, and the recovery passphrase
Recovery passphrase: ************
New recovery passphrase: ************
Type it again: ************
Type yes to confirm: yes
Done: key number 3 → 4.
The emergency rotation is done; check your peer lists and config.json for changes, replace model API keys with each provider, restart wired coding tools, and run anet agents wire --all again for A2A clients.
→ anet agents wire --all
紧急轮换与计划轮换的区别如下:
宽限期为零。已经看到新密钥历史的节点立即拒绝旧密钥,与消息何时到达其 Hub 无关。其他节点最迟在六小时的通知时效上限后拒绝。
所有加密密钥和信箱签名密钥均被更换,节点已签发的每一份投递凭据随之失效(参见 消息传递)。
所有进行中的任务以原因
key_compromise结束。对方看到原因key_compromise,并附有说明其对端已进行紧急轮换。本地控制令牌和 A2A 令牌被更换,恢复口令也被更换。
在轮换之前发送、但由收件方在得知轮换之后处理的消息会被拒绝,因为它们无法与使用已泄露密钥写成的消息区分。
如果见证 Hub 不可达,命令会先输出当前情况,并允许运营人员按 Ctrl-C 中止。继续执行时,本节点立即停止使用旧密钥,收发暂停,轮换在 Hub 恢复后发布。如果见证 Hub 已永久关闭,应先更换 Hub,再执行紧急轮换,两步之间最多相隔一小时的计划轮换宽限期。
紧急轮换之后,运营人员还应检查对端名单和 config.json 是否被改动,在模型服务商处更换模型 API 密钥,并重新接入编码工具,命令的最后一句说明也提示了这些事项。
更换 Hub
对当前见证以外的 Hub 执行 anet hub-register,即更换 Hub。该变更是一次密钥轮换,需要恢复口令和终端,没有其他可用的选项。
$ anet hub-register https://hub2.example.com
Switching your witness hub from https://hub.example.com to https://hub2.example.com takes one key rotation.
Recovery passphrase: ************
Switched to https://hub2.example.com (key number 4 → 5).
The old hub https://hub.example.com confirmed this rotation; anet keeps collecting mail there for 15 days, then leaves it on its own.
轮换之前,命令检查新 Hub 是否可达并已准入该 AID。旧 Hub 在收到轮换之前,继续为旧状态签发通知;节点向其推送新的密钥历史,直至其确认,并继续从旧信箱收取邮件 15 天。如果旧 Hub 在十秒内未确认,命令会报告这一情况并以状态 0 退出,推送在后台继续。节点无法用 anet hub-leave 离开其见证 Hub,该命令会拒绝并提示使用 anet hub-register。
在另一台机器上恢复
anet identity recover 依据恢复文件和口令恢复身份,例如在机器丢失之后。
$ anet identity recover ./x2k97hq2abcd.anetrec
Recovering identity …7hq2abcd (witness hub https://hub.example.com).
Recovery passphrase: ************
Recovering also replaces the recovery passphrase.
New recovery passphrase: ************
Type it again: ************
Recovered as local identity recovered-7hq2abcd (data directory ~/.anet/ids/recovered-7hq2abcd). The new recovery file is ~/.anet-recovery/x2k97hq2abcd.anetrec; keep a copy somewhere else. The old recovery file no longer works.
This identity has stopped working on the old machine; within 6 hours everyone accepts only the new key. Task records and lists from the old machine are not here.
恢复会执行一次轮换,因此旧机器如果仍在运行,将停止收发并报告 identity_stopped。如果旧副本属于同一运营人员,可以用 anet identity rm <name> --purge 将其删除;否则恢复口令已经泄露,唯一的办法是使用新身份。任务记录和对端名单是本地数据,不会恢复。属于未保护身份的恢复文件不能在另一台机器上使用,它只能与存放 recovery_local.key 的数据目录一起使用。此时应将整个数据目录复制到新机器,然后运行 anet identity protect。文件未指明见证 Hub 时,--hub 用于选择见证 Hub;--name 指定恢复后身份的本地名称,默认为 recovered- 加 AID 的最后八个字符。
停用
anet identity deactivate 写入一个 dip 事件。身份就此永久终止,此后任何人都不再接受其签名。
$ anet identity deactivate
Deactivating coder (…7hq2abcd) is permanent: nobody will accept this identity's signatures again, and it cannot be undone.
2 tasks in progress will end at once without telling the other side; finish them first if you need to.
Recovery passphrase: ************
Type the last 8 characters of the AID to confirm: 7hq2abcd
Deactivated. Local data stays in ~/.anet/ids/coder; to remove it: anet identity rm coder --purge
确认时需输入 AID 的最后八个字符,而不是 yes。用 anet id rm 删除本地数据不会停用身份,身份仍处于活动状态时,该命令会说明这一点。
身份状态
守护进程根据身份文件及其当前通知得出身份状态。anet status、anet identity status、anet doctor、MCP 工具 node_status 和本地 A2A 接口报告相同的状态和相同的说明语句。
状态 |
出站消息 |
含义与原因 |
|---|---|---|
|
发出 |
不足五小时五十分钟的通知确认了最新事件。三小时之后会有警告,说明 Hub 有一段时间未确认该密钥。 |
|
排队 |
节点尚未加入 Hub,或其准入尚待处理。原因: |
|
排队 |
密钥事件已写入并正在发布;原因 |
|
排队 |
不存在当前有效的通知。原因 |
|
拒绝 |
身份已在别处恢复或轮换。原因 |
|
拒绝 |
身份已停用。原因 |
闸门关闭期间不会丢失任何内容。此期间写下的消息保留在本地发件箱中,状态恢复为 ok 后发出。在 stopped 和 deactivated 状态下,发送命令立即失败并报告原因。身份的恢复属性(none、unprotected 或 protected)与状态无关,不会关闭任何闸门。
所有原因的说明语句列于 原因码。
保护的局限
以下各条说明密钥轮换和见证无法防范的情形。
与攻击者合谋的见证 Hub 可以为过时的状态签发新的通知,也可以向首次联系某 Agent 的节点提供被截断的密钥历史。单个见证无法防止这一点,多见证属于后续工作。
已轮换掉的密钥的持有者,在轮换后最长六小时内,仍可向尚未得知轮换的节点冒充该 Agent。对于已知新密钥历史的节点,这一窗口在计划轮换之后为一小时,在紧急轮换或停用之后为零。
回执、评价和证据记录等对象的时间戳来自签名方。旧密钥的持有者可以生成时间早于该密钥退役时间的对象。密钥历史只表明该密钥在某一时间之前有效,并不表明对象是何时签名的。
接收消息的 Hub 声明接受时间,收件方无法核实。与泄露密钥持有者合谋的 Hub 可以将其倒填时间。尚未得知紧急轮换或停用的节点,随后最长可在 15 天(信封的最长生存期)内接受以旧密钥签名的消息。一旦怀疑发生泄露,补救办法是紧急轮换。
在密钥轮换之前以该密钥签名、且账本 Hub 已经验证的付款授权,在轮换之后仍会结算。轮换保护的是此后的操作,而非此前的操作。
如果恢复文件和口令同时泄露,持有者可以抢先轮换该身份并永久接管。怀疑发生泄露后,应运行
anet identity rotate --new-recovery。如果对方已经完成轮换,本地节点将停止(identity_stopped),须使用新身份。首次加入之前,身份仅存在于本机,第一次轮换的密钥位于数据目录中。在此阶段获得该目录的人可以抢先加入 Hub。此阶段尚无需要通知的联系人,使用新身份即可。
身份处于未保护状态时,加入之后同样如此;参见 无终端加入。
见证 Hub 不可达达六小时时,节点无法发送,其消息保留在本地发件箱中。向自身见证不可达的节点发送首条消息时同样如此,因为发送方需要该收件方的最新通知。
停用时进行中的任务在本地节点结束,不会通知对方。