如果说大模型把“对话能力”普及到了每一个开发者,那么接下来真正改变工作流的,是 Agent 开始替我们做事。但面对 Agent 的爆发,有一个基础设施一直被低估:身份。
当你教一个 Agent 去“联系 Alice”“校验 Bob 发的配置”“确认某个维护者是否值得信任”时,它第一步要做的事情是什么?是判断面前这段文本到底是谁写的。人类可以看头像、看历史、看说话风格,Agent 不行,它需要一种可以程序化验证的信任链路。
传统互联网身份体系是围绕“登录”设计的,密码、验证码、OAuth 跳转、会话 Cookie,每一步都是为了证明“当前操作者是谁”。但 Agent 的工作方式不太一样,它更接近“读者”和“执行者”的叠加态:它要一边读你发布的身份信息,一边确认这些信息没有被中途篡改,一边决定要不要进一步操作。于是有人提出了一个很朴素的方案:Username.md——一个经过签名的、Agent 可读的、由你自己拥有的身份页面。
这个想法并不是要给整个 Web 重新发明一套身份协议,而是用已经成熟的 Markdown + PGP 签名 + 自有域名组合起来,解决一个非常具体的问题:Agent 拿到你的身份页时,如何确信页面里的内容真的出自你手。这篇文章会拆解它的设计思路,把它和主流的身份方案放在一起对比,然后从密钥生成、页面编写、签名发布到验证脚本,完整走一遍实战流程。
1. 这篇文章真正要解决的问题:AI Agent 的验证困境
先看几个真实场景。
第一个场景是多 Agent 协作。假设你的 Agent 需要从同事的站点上读取一份 API 接入说明,而这份说明是同事的 Agent 自动生成的。双方都是机器,没有浏览器、没有验证码、没有人工审核。你的 Agent 拿到内容后,要先问一个问题:这份说明真的来自同事本人吗?会不会是中间环节被替换的?在没有额外验证手段的情况下,Agent 只能选择相信或不相信,而“盲目相信”在生产环境里是灾难。
第二个场景是开源身份。开源维护者通常会把自己的 GitHub 主页当作身份入口,但 GitHub 主页本质上是一个平台托管的页面。平台可以限制账号、可以关停服务、可以改版。如果你的 Agent 依赖某个维护者的 GitHub 页面来获取公钥和联系方式,平台侧的任何一个变化都会破坏这条信任链。对 Agent 来说,它更希望看到一个不依赖平台、可以长期稳定访问的固定端点。
第三个场景是自动接入。过去一个人要建立一份“个人资料”,是给 HR 或合作方看的;现在这份资料还要给 Agent 看。Agent 需要解析你的名字、你的公开邮箱、你的公钥指纹、你的社交链接。它甚至需要知道“该信任哪一份资料、哪个公钥”。如果这些信息散落在社交主页、博客、简历、登记表里,Agent 每次都要靠网页抓取和语义理解来猜,效率低,还容易出错。
这三个场景指向同一个结论:Agent 时代对身份验证的需求,和人类时代有本质区别。人类验证身份靠交互,Agent 验证身份靠密码学。你需要证明的不是“我能在浏览器里登录”,而是“这段内容由一个我信任的私钥持有者签名,并且内容自发布后没有被修改过”。这就是 Username.md 这类方案要补上的空白。
所以这篇文章真正要解决的问题,一句话概括:如何用最低成本,为自己搭建一个 Agent 可以直接读取、又能用密码学方法验证的本人身份页。
2. Username.md 是什么:一个“签名 + 可读 + 自有”的身份页
从项目名拆开看,Username.md 包含三层设计:Username 表示它代表一个用户身份标识;.md 表示内容格式是 Markdown;而项目标题里的三个定语,才是它的技术核心。
2.1 signed:签名解决的是内容可信问题
“signed”意味着身份页不是一封来自服务器的纯文本,而是由你本地私钥签名过的数据。
签名解决两个问题。第一是内容完整性。身份页发布后,只要任何一个字节被改动,签名验证就会失败。第二是来源确定性。只要私钥没有泄露,能够验证通过的内容就可以确定为私钥持有者所发布。
这里要和“加密”做一个区分。加密是为了防止别人看到内容,签名是为了让别人确认内容来源可信,两者使用私钥的方式不同。身份页通常是公开信息,不需要加密,但必须有签名。用生活化的比喻来说,签名相当于“数字公证”:文件摆在那里谁都能看,但封条上的签章只有你才有。
2.2 agent-readable:让 AI 能以最低成本读懂身份信息
Agent 读取网页和人类不同。人类可以理解复杂排版、图标、图片中的含义,Agent 如果要稳定解析,更希望看到结构化程度高、噪声少的文本。纯 Markdown 就是一个非常合适的选择。
一方面,Markdown 比 HTML 轻量得多,没有 script、style、div 嵌套等无关内容;另一方面,Markdown 本身有标题、列表、链接的语义,Agent 可以很容易识别出“这个段落是身份声明”“这一行是联系方式”“那个链接是社交主页”。如果身份页是动态渲染的 SPA 页面,Agent 还需要执行 JavaScript 才能拿到真正内容,这会让解析成本大幅上升。
所以“agent-readable”不只是一个格式偏好,更是一个工程决策。固定 URL + 纯静态 Markdown + 独立签名文件,意味着 Agent 可以用三行命令完成抓取、验证、解析的全部流程。
2.3 you own:身份数据不随平台迁移而消失
“you own”是这套设计里容易被低估的部分。它的含义是:身份页处于你控制的域名之下,签名私钥也由你保管。平台可以关停你的账号,搜索引擎可以调整收录策略,但只要你还有域名和私钥,你的身份声明就依然存在。
从更深一层看,这其实是把“身份数据”和“平台账号”解耦。现在很多人的数字身份绑定在微博、GitHub、微信等平台上,平台注销账号等于注销了身份。而 Username.md 式的设计,让身份页本身成为一种可以迁移、可以备份、可以长期持有的资产。
当然,这里要澄清一个边界:域名和私钥属于你,不等于“这个身份对应的现实实体”自动被验证。域名和私钥只能证明内容来源稳定,不能证明现实世界的你叫什么名字、在哪个公司工作。身份和实体之间的连接,通常还需要其他可信凭证来背书。这个边界在后面章节会展开。
3. 传统身份方案在 Agent 场景下的短板
把 Username.md 和几种主流身份标识放在一起对比,能更清楚地看出它的定位。
| 方案 | 身份归属 | 机器可读性 | 签名验证 | 长期可用性 | 适合 Agent 吗 |
|---|---|---|---|---|---|
| GitHub / 社交主页 | 平台 | 一般 | 无 | 受平台限制 | 较弱 |
| 个人博客 about 页 | 自己 | 差 | 无 | 强 | 一般 |
| OAuth / OIDC | 身份提供商 | 强 | 协议自带 | 强 | 适合登录鉴权,不适合内容信任 |
| VCard / JSON-LD | 自己 | 强 | 无 | 强 | 适合结构化数据,无防篡改 |
| PGP Web of Trust | 自己 | 弱 | 强 | 强 | 需要额外基础设施 |
| Username.md | 自己 | 较强 | 强 | 强 | 比较契合 |
细看几个代表方案。
OAuth / OIDC 是很成熟的身份协议,但它的设计目标是“授权”和“登录”,流程中有重定向、会话、Token 交换。Agent 如果只是想读一个公开身份页,用 OAuth 显然是杀鸡用牛刀。而且 OAuth 依赖身份提供商,如果 Agent 要对接多个方,就需要处理多个提供商的协议差异。
JSON-LD 和 VCard 在机器可读性上很强,结构非常规整,但它们通常不包含签名机制。Agent 能解析出“这个节点代表邮箱”并不意味着“这个邮箱是我声明的那个邮箱”。缺少签名,内容可信度就得不到保障。
PGP 的 Web of Trust 设计目标是解决公钥信任链,签名能力很强,但整体体系复杂,普通 Agent 很难直接消费。Username.md 没有重新发明一套公钥基础设施,而是把 PGP 的签名能力直接嵌入到一个 URL 里,让消费方只需要做一次gpg --verify。
从材料看,Username.md 的定位更像是“轻量级个人身份端点”:它在机器可读性和密码学可验证性之间取了平衡,同时保留了部署的简单性。它解决的核心矛盾是:一些方案太复杂,一些方案又太不可信,而开发者需要一个刚刚好的中间点。
4. 环境准备与前置条件
搭建 Username.md 式的身份页,不需要很重的环境,用到的都是比较通用的工具和协议。下面是建议的准备工作。
- 操作系统:Linux 或 macOS 最方便;Windows 用户建议使用 WSL 或 Git Bash,避免 GPG 路径和命令差异带来的问题。
- GnuPG:用于生成密钥、签名、验证。通常系统自带,也可以用
gpg --version确认;版本以实际环境为准,本文演示的是通用命令。 - 域名或静态托管:自建用户名身份页,最好放在自己控制的域名下。没有域名时,用 GitHub Pages、Cloudflare Pages、Vercel 等静态托管也能先用起来,但自主性会弱一些。
- Markdown 编辑器:任何文本编辑器都可以,建议用 VS Code 或 Typora,方便预览。
- Python 3:用于编写自动化验证脚本;如果不想写脚本,也可以直接用 curl 加 gpg 命令手动验证。
- git:用来管理身份页内容和签名历史,方便回溯每次变更。
环境准备阶段不需要安装任何重量级框架。整套方案的依赖非常轻,这也是它适合个人开发者快速落地的原因。
5. 核心流程拆解:从密钥生成到签名发布
整个流程可以拆成五步:生成密钥、编写身份页、生成签名、发布内容、验证结果。每一步都有对应的命令和注意事项。
5.1 生成 GPG 密钥对
身份页的签名能力来自一个你自己保管的 GPG 密钥对。如果之前已经有 GPG 密钥,可以直接复用;如果没有,需要先生成一个新的。
gpg --full-generate-key交互式界面会要求选择密钥算法、密钥长度、有效期、用户 ID 等。对于身份页场景,选择 RSA 4096 位是比较稳妥的默认值,有效期可以根据自己的维护习惯设定,比如三年或五年。用户 ID 建议使用真实姓名和常用邮箱,因为这个信息会被 Agent 展示给其他调用方。
生成完成后,查看密钥列表和指纹:
gpg --list-secret-keys --keyid-format LONG gpg --fingerprint <KEY_ID>指纹是密钥的一个全局唯一标识,也是你后续要在身份页里公开给 Agent 的值。注意,私钥必须保存在本机,并且设置密码保护;它不应该被上传到服务器、仓库或任何第三方平台。
5.2 编写身份页内容
身份页是一个 Markdown 文件,建议命名为username.md,放在网站的固定路径下。虽然叫 username,但里面的内容不只是一个用户名,而是完整的身份声明。推荐包含这些字段:显示名称、个人网站、公开邮箱、PGP 公钥指纹、社交账号、个人偏好等。
一个需要注意的设计点是:身份页的内容要尽量稳定。Agent 可能会缓存它,也可能在多次请求之间做比对。如果你频繁改动内容,一方面会让签名难以追溯,另一方面也可能影响 Agent 对身份的信任判定。建议把身份页当作一个“版本化”的资产来维护,而不是一个普通的博客页面。
5.3 生成签名文件
身份页写好之后,对文件做一次分离签名:
gpg --detach-sign --armor username.md执行后会在同目录生成username.md.asc,这个文件就是签名。所谓“分离签名”,是指签名独立存放在另一个文件中,原始 Markdown 文件保持纯净。
为什么要用分离签名而不是把签名嵌入到 Markdown 里?核心原因是 Agent 读取的 URL 应当是纯 Markdown,这样 Agent 不需要解析 PGP 包裹格式就能直接读取正文。如果使用gpg --clearsign生成内联签名,虽然签名和内容在一个文件里更方便人工下载,但 Agent 必须先剥离文件头尾的签名块才能拿到正文,增加了解析成本。所以对agent-readable的要求来说,分离签名是更优选择。
验证分离签名时,gpg 会要求同时提供签名文件和被签名的数据文件:
gpg --verify username.md.asc username.md当输出包含Good signature时,说明内容确实由对应私钥持有者签名,且内容未被篡改。
5.4 发布到自己的域名
把username.md和username.md.asc一起发布到你的网站根目录或者某个固定路径,比如https://yourdomain.com/username.md。
如果使用 GitHub Pages,操作很简单:把两个文件提交到仓库,开启 Pages 服务即可。如果使用自有服务器,则把文件放到 Web 服务的静态目录下。以 nginx 为例,只需要保证两个文件能被直接访问,不经过任何动态处理。
这里有一个容易踩坑的地方:有些 Web 框架或 CDN 会对.md文件做额外处理,比如转成 HTML、添加渲染模板。但对 Agent 来说,我们需要的是原始 Markdown 文本,而不是 HTML 渲染结果。因此发布时务必确认访问https://yourdomain.com/username.md返回的是原始文本,而不是页面源码。可以使用curl -s https://yourdomain.com/username.md检查响应内容。
5.5 在 Agent 或脚本中完成验证
发布完成后,消费方可以按下面三步来验证页面:
- 用固定 URL 下载身份页和签名文件。
- 用公钥验证签名。
- 签名通过后,解析 Markdown 内容并用于后续业务。
如果消费方是你的自定义 Agent,可以把公钥指纹预先配置在 Agent 的信任名单中。首次信任可以由人工确认一次,后续验证都基于这个已信任的指纹。这个流程和首次使用 SSH 时的 “known_hosts” 机制非常相似。
6. 完整示例与代码实现
下面给出一个可以直接复制的完整示例。假设你的域名是https://alice.example,身份页文件名为username.md。
6.1 身份页 Markdown 示例
文件路径:username.md
# username: alice - display_name: Alice Zhang - website: https://alice.example - email: alice@example.com - pgp_fingerprint: ABCD EF12 3456 7890 ABCD EF12 3456 7890 ABCD EF12 ## identity 本站点用于声明 Alice Zhang 在互联网上的身份信息,以及对应的 PGP 公钥指纹。 所有内容通过 PGP 签名,验证签名即可确认页面内容未被篡改。 ## contact - email: alice@example.com - github: https://github.com/alice - blog: https://alice.example/blog ## preferences - preferred_chat: matrix - timezone: Asia/Shanghai - language: zh-CN这个文件的优点在于:人类直接打开可以看到完整身份信息;Agent 按 Markdown 语法解析,可以稳定提取display_name、email、pgp_fingerprint等字段。使用- key: value这种列表形式,比纯叙事段落更容易被解析。
6.2 签名与发布命令
进入username.md所在目录,执行分离签名:
cd ~/identity-page gpg --detach-sign --armor username.md生成username.md.asc后,把两个文件提交到发布目录。如果是 git 管理的静态站点,可以执行:
git add username.md username.md.asc git commit -m "update identity page and signature" git push origin main如果使用 nginx 部署,把文件放到设置的静态目录中就可以了。为了控制 Agent 的缓存行为,可以加一段简单的响应头配置:
location = /username.md { add_header Cache-Control "public, max-age=3600, must-revalidate"; } location = /username.md.asc { add_header Cache-Control "public, max-age=3600, must-revalidate"; }这里设置的是 1 小时缓存,must-revalidate保证内容更新后,Agent 可以通过重新验证签名拿到最新状态,避免因为旧缓存而信任过期内容。
6.3 Python 验证脚本
当 Agent 消费身份页时,可以把验证逻辑封装成一个 Python 脚本。下面脚本从指定 URL 下载身份页和签名,然后调用本机 gpg 命令完成验证。
#!/usr/bin/env python3 import os import sys import tempfile import subprocess import urllib.request BASE_URL = "https://alice.example" CONTENT_PATH = "/username.md" SIGNATURE_PATH = "/username.md.asc" def download(url: str) -> bytes: with urllib.request.urlopen(url, timeout=10) as resp: return resp.read() def main() -> None: with tempfile.TemporaryDirectory() as tmp: content_file = os.path.join(tmp, "username.md") sig_file = os.path.join(tmp, "username.md.asc") with open(content_file, "wb") as f: f.write(download(BASE_URL + CONTENT_PATH)) with open(sig_file, "wb") as f: f.write(download(BASE_URL + SIGNATURE_PATH)) result = subprocess.run( ["gpg", "--verify", sig_file, content_file], capture_output=True, text=True, ) print(result.stdout or result.stderr, end="") if result.returncode == 0: print("[OK] signature verified: content has not been tampered with") else: print("[FAIL] signature verification failed") sys.exit(1) if __name__ == "__main__": main()脚本逻辑很直接:下载两个文件,调用gpg --verify做验证,再根据返回码输出结果。执行方式:
python3 verify_identity.py将这个脚本作为 Agent 工具链的一部分,可以在 Agent 使用身份页内容之前先跑一次验证,保证读到的是可信内容。
7. 运行结果与效果验证
执行上面的 Python 脚本,如果一切正常,输出会和下面类似:
gpg: Signature made Mon DD HH:MM:SS YYYY CST gpg: using RSA key ABCD EF12 3456 7890 ABCD EF12 3456 7890 ABCD EF12 gpg: Good signature from "Alice Zhang <alice@example.com>" [OK] signature verified: content has not been tampered with验证成功的标准是:
- 命令行输出包含
Good signature。 - 脚本返回码为 0。
- 页面里的
pgp_fingerprint和本地验证密钥的指纹一致。
如果验证失败,第一步判断信号是gpg的具体报错:
- 输出
Can't check signature: No public key,说明本机没有导入对应的公钥,需要先从页面或信任节点获取公钥。 - 输出
BAD signature,说明内容被篡改过,或者签名文件和内容文件不匹配。 - 输出
Good signature,但提示日期不受信任,说明签名时间距离当前时间过远,或者密钥有效期已过。
在接入 Agent 之前,建议先手动验证一次,确认 URL 路径、签名文件和公钥指纹这三个环节都是通的。手动验证跑通后,再把验证逻辑接入 Agent 的调用链,这样可以减少排错时的不确定因素。
8. 常见问题与排查思路
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
验证时报No public key | 本机没有导入身份页对应的公钥 | 执行gpg --list-keys查看本地公钥列表 | 导入发布者的公钥后再验证 |
验证报BAD signature | 内容被修改,或签名文件与内容不配对 | 对比两端文件哈希,确认 URL 对应关系 | 重新生成签名并发布 |
Good signature但指纹不一致 | 页面声明的指纹与密钥实际指纹不一致 | 执行gpg --fingerprint比对 | 更新页面信息,或更换密钥 |
| Agent 解析不到正文 | 访问 URL 返回了 HTML 渲染页而不是原始 Markdown | curl -s URL直接查看响应文本 | 关闭服务端的渲染转换,或改用专门的静态文件路径 |
| 签名时间过旧 | 内容发布后没有再签名 | 查看签名时间戳 | 更新内容后重新签名 |
| 页面访问 404 | 文件路径或文件名不对 | 用浏览器访问 URL,检查静态目录 | 修正发布路径,确认文件名大小写 |
| 密钥疑似泄露 | 私钥文件可能被外部访问 | 检查私钥备份情况和密钥生成记录 | 立即吊销密钥,生成新密钥并更新所有页面 |
排错的总原则是:先从最容易验证的环节开始。先确认 URL 能访问,再确认公钥已导入,最后观察gpg --verify的输出。这三个环节任何一个不通,后面都谈不上信任。
9. 最佳实践与工程建议
把 Username.md 这类身份页真正用起来,有几点工程层面的建议值得记住。
第一,固定路径比固定内容更重要。身份页 URL 一旦公开,尽量不要频繁变更。Agent 会把 URL 当作身份标识的一部分记住,如果你今天放在/username.md,明天改成/about.md,旧引用的验证链路就会全部失效。更稳妥的做法是:确保旧 URL 能在相当长的时间内保持可访问,即使页面内容更新,路径也不变。
第二,私钥是身份的核心资产。私钥一旦泄露,别人就能伪造你的所有身份页。建议私钥设置高强度密码,并做离线备份。不要把私钥放进 git 仓库,不要上传到服务器,不要放在公开的 CI 配置中。签名操作只在本地完成,服务器上只放公钥和已签名的文件。
第三,定期轮换密钥并保持旧密钥可验证一段时间。密钥到期后,不要直接删除旧公钥。代理节点可能还保存在旧密钥的缓存,突然失效会导致验证报错。更好的做法是:新密钥发布后,保留旧公钥一个过渡期,同时在身份页里注明新旧密钥的替换关系。
第四,内容更新和签名更新要保持原子性。每次更新username.md后,必须立刻重新生成签名文件,并且成对发布。如果只更新内容不更新签名,所有 Agent 都会验证失败。发布前可以用脚本自动检查“文件变更时间是否晚于签名时间”,或者直接做成一个发布脚本,把“重新生成签名”固定在提交流程里。
第五,用 git 管理身份页的历史版本。身份页是长期资产,每次修改都应有记录。建议每个版本都有对应的签名,这样在纠纷发生时,你可以拿出历史版本证明“某段时间内的身份声明是什么”。
第六,不要过度依赖单一信任节点。即使你的域名、私钥都在自己手里,DNS 被劫持或域名过期仍然会影响身份页访问。建议在身份页上同时公布多个可验证信息,比如公钥指纹、邮箱的 WKD 记录、社交账号上的公钥公告,让 Agent 有多个交叉验证的入口。
第七,对 Agent 消费方来说,首次信任建立是关键。第一次验证某个人的身份页时,公钥指纹的来源需要人工确认。确认之后,建议把指纹写入 Agent 的配置或数据库中,后续的验证都以这个已确认的指纹为准,而不是每次从页面读取指纹。
10. 总结与后续学习方向
Username.md 这个项目之所以值得关注,是因为它没有引入任何复杂的协议,只是把三个已经成熟的能力组合了起来:Markdown 作为机器可读的格式,GPG 签名作为密码学验证手段,自有域名作为数据控制权边界。对一个 Agent 开发者来说,这套组合意味着你可以用最低成本,为你的 Agent 建立可验证的身份信任链路。
从更长远的视角看,Agent 时代会需要更多类似的基础设施。当前这套方案仍然有一些待完善的地方,比如公钥的首次信任如何自动化、多密钥轮换的标准流程是什么、身份页是否可以声明可授权的 Agent 能力边界、是否需要一个轻量的键值发现协议来帮助 Agent 定位身份页路径。这些都是值得继续深入的方向。
对你来说,现在最值得做的实验,不是继续收集更多 Agent 框架或提示词技巧,而是先把自己的身份页搭起来。用域名、GPG 密钥和一个 Markdown 文件,把“我是谁、我的公钥是什么、我的联系方式是什么”以可验证的方式发布出去。等 Agent 真正开始跨系统协作时,你会在第一步就理解:信任不是靠协议名称堆出来的,而是靠签名的每一次成功验证建立起来的。