☰
Username.md:用Markdown和签名打造AI Agent可验证的自主身份页
2026/11/4 21:54:23 网站建设 项目流程

Username.md 这个项目,简单说就是想让你拥有一个属于自己的、经过签名认证、并且能被 AI Agent 直接解析的身份页。它不依赖某个社交平台,也不把身份数据锁在数据库里,而是用一页 Markdown 文件来表达:你是谁、你的公钥是哪个、你允许 Agent 怎么验证你并联系你。

如果你关注去中心化身份、个人数据主权,或者正在做 Agent 相关的信息抓取和信任判断,这个方向很值得拆一遍。它和传统个人主页最大的区别是两个关键词:signed 和 agent-readable。前者解决“内容有没有被篡改”,后者解决“机器能不能低成本地消费这份身份信息”。

下面我会按实际落地顺序,从页面结构、签名流程、Agent 解析、托管发布到排查边界完整过一遍。其中有些做法是项目定位直接指向的,有些是通用实践补充,落地时根据你的场景调整即可。

1. 先搞懂它解决的“身份页”问题

1.1 身份页本身不新鲜,新鲜的是信任机制

个人主页这个概念已经存在二十年了。访客、留言板、个人介绍、链接收集,本质上都是身份页。问题在于传统身份页有几个长期没有解决的痛点:

  • 内容托管在平台上,平台删号或者关站,身份信息就消失了。
  • 内容无法验证作者,任何人都可以复制一份别人的主页,改个名字然后冒充。
  • 跨平台身份很难关联,GitHub 上的你、邮箱背后的你、博客里的你是不是同一个人,没有统一答案。

Username.md 的定位,是把身份信息放回你控制的域名或存储空间,用标准的 Markdown 文件承载,再用密码学签名给这份文件盖章。这样身份页的“所有权”从平台回到了个人手里,而“真实性”从平台审核变成了可验证的签名机制。

1.2 signed 解决的是完整性和来源问题

签名解决的核心问题不是“你是谁”,而是“这份内容有没有被改过”。当你用私钥对 username.md 的内容生成签名后,任何人只要拿到你的公钥,就能验证这份文件是否完整、是否由持有对应私钥的人发布。

这和 HTTPS 的证书机制思路类似,但更轻量。Username.md 并不强制你接入某个 CA,而是采用“公钥即身份”的模式:在我认可你的公钥之前,这份签名只能证明内容自洽;在我认可公钥之后,签名才成为可信的来源标记。

这里要提前说清楚一个边界:签名能防篡改,能防“内容被中间人换掉”,但不能单独证明“页面背后一定是个真人”。身份真实性还需要别的信任锚,比如你的域名所有权、社交账号互链、或者你在其他场景下公开使用同一把公钥。这一点后面专门展开。

1.3 agent-readable:为什么机器可读突然变重要了

现在的 AI Agent 越来越多地被要求去网上获取信息,比如查一个人是谁、联系方式是什么、有没有公开声明。传统网页是为人类阅读设计的,充斥着导航、广告、动态脚本和无关内容,Agent 抓回来的页面噪声很大,解析成本很高。

Username.md 想解决的就是这个问题:用 Markdown 这种通用纯文本格式承载身份信息,配合结构化前部字段,让 Agent 可以像读配置一样读取身份页。机器先验签,再提取字段,然后决定是否把这份身份信息纳入自己的判断依据。

所以它并不是要做成一个“更好看的个人主页”,而是要做成一个“人和 Agent 都能消费的身份文件”。这个定位直接影响后续所有的格式选择和签名设计。

2. 一个 Username.md 身份页应该包含什么

2.1 文件命名与位置约定

从项目名来看,核心文件就是username.md,其中 username 是你的身份标识。实际部署时有两种常见方式:

  • 放在你自己的域名根目录,比如example.com/username.md。
  • 放在域名下的约定路径,比如example.com/.well-known/username.md,表示这是一个机器可读取的元数据文件。

如果你没有独立域名,也可以放在静态托管平台生成的页面上,比如 GitHub Pages 项目页。关键是这个 URL 要稳定、要公开、要能长期访问。身份页最怕的就是 URL 经常变,Agent 和人都需要一个能收藏的稳定地址。

2.2 人机两读的格式设计

Markdown 本身是给人读的,它天然支持标题、列表、段落,阅读体验很好。但机器解析纯 Markdown 并不容易,因为它没有强制的结构。所以实践上通常会在 Markdown 文件顶部加一段 YAML frontmatter,也就是被---包围的键值对区域。

这种格式在 Jekyll、Hugo、Obsidian 等工具里已经很常见。Username.md 采用类似思路,就可以复用大量现成解析器,Agent 不需要从头写一套 Markdown 结构解析引擎。

frontmatter 放机器字段,正文放人类可读的介绍,这样一份文件两种受众都覆盖。

2.3 最小字段集应该有哪些

根据“身份页 + 签名 + Agent 可读”这三个目标,我建议最少包含以下字段:

字段作用建议格式
name显示名称普通字符串
handle用户名标识小写字母数字,可包含连字符
updated内容更新时间ISO 8601,如 2025-01-15T10:30:00Z
keys公钥信息OpenPGP 指纹或 SSH 公钥字符串
links关联身份地址多个 URL,如主页、GitHub、邮箱
claims声明或职位信息简短文本列表
signature签名入口签名文件的 URL 或内嵌签名串

字段不要贪多。身份页不是简历,不要塞几十个键值对。Agent 需要的是核心身份信息,你暴露越多字段,后续维护和防泄露压力就越大。

2.4 一个最小示例

下面是一个参考结构,实际内容按你的身份信息替换:

--- name: "张三" handle: "zhangsan" updated: "2025-01-15T10:30:00Z" keys: pgp: "ABCDEF1234567890ABCDEF1234567890ABCDEF12" links: homepage: "https://example.com" github: "https://github.com/zhangsan" email: "mailto:zhangsan@example.com" claims: - "独立开发者" - "关注开放身份协议" signature: "https://example.com/username.md.sig" --- # 张三 你好,我是张三,一名独立开发者。 这个页面是我的身份页,用于向人类访客和 AI Agent 提供经过签名的个人身份信息。 ## 联系我 - 邮箱:zhangsan@example.com - GitHub:https://github.com/zhangsan

这里注意一点:签名文件放在 frontmatter 里用 URL 引用,或者直接把 base64 签名串塞进去,两种都有人用。引用方式更干净,文件本身不会因为签名变化而改动正文;内嵌方式更自包含,单文件就能完成验证,但签名串会占用不少空间。对 Agent 来说,两种都能处理,只要解析逻辑一致。

3. 签名与验证的具体流程

3.1 选哪种签名工具

不涉及某个固定实现的话,常见选择有 OpenPGP(GPG)、SSH 密钥、Minisign 等。三者的差异可以参考下表:

工具上手难度生态普及度适合场景
GPG/OpenPGP中等高,邮件和软件签名常用完整身份签名、密钥链管理
SSH 密钥低高,开发者和服务器场景熟悉轻量验证,适合技术人员
Minisign低中等,签名文件简单快速给文件签名和验证

如果你是第一次做这件事,我建议先用 SSH 密钥体验一遍签名和验证的闭环,因为大多数开发者本地已经有 SSH 密钥,不需要额外生成。如果后续要做长期身份管理,再考虑引入 GPG 的密钥链机制。

3.2 用 GPG 给 Markdown 文件签名的流程

以 GPG 为例,核心步骤是三条命令:

# 生成密钥,如果已经存在可以跳过 gpg --full-generate-key # 对身份页生成独立的 ASCII 签名文件 gpg --detach-sign --armor --output username.md.sig username.md # 验证签名 gpg --verify username.md.sig username.md

这里有一个非常重要却容易被忽略的点:签名针对的是原始 Markdown 文件,不是渲染后的 HTML,也不是复制到剪贴板的文本。因为签名的本质是对文件字节做哈希和加密,任何形式的格式转换、行尾替换、末尾追加换行,都会导致验证失败。

3.3 验证方应该按什么顺序校验

Agent 或者人类访客拿到一份 username.md 后,合理的验证顺序是:

  1. 下载username.md和签名文件username.md.sig。
  2. 从页面字段中读取声明的公钥指纹或公钥内容。
  3. 从公钥服务器或用户提供的公钥 URL 获取公钥,并确认指纹一致。
  4. 执行验签命令。
  5. 验签通过后,再信任页面里的字段内容。

这个顺序不能乱。如果你先解析内容再验签,解析器可能已经被污染;如果你不核对公钥指纹就导入,就可能用了攻击者提供的假公钥。验签不是“跑一下命令看是否成功”,而是“确认公钥来源可信”和“确认签名有效”两件事必须同时成立。

3.4 密钥更换与时间戳问题

身份页是长期存在的,密钥却可能因为泄漏、丢失或加密强度升级而更换。建议做法是:

  • 页面里保留旧的公钥指纹,形成一条“我曾用过哪些钥匙”的记录。
  • 新身份页用新私钥签名,同时用旧私钥对新公钥做一次交叉签名,让连续信任关系成立。
  • 在 keys 字段中标注每个公钥的起始和结束时间,方便 Agent 判断当前应该信任哪把钥匙。

时间戳方面,如果环境允许,可以考虑接入 RFC 3161 时间戳服务,把“签名发生在某个时间点”固化下来。但这一步比较复杂,普通身份页可以先不做。至少保证页面里的updated字段能反映最新的修改时间。

注意:密钥的私钥部分一定要离线保存,不要放到云端笔记或代码仓库里。身份页签名私钥一旦泄露,攻击者就能伪造你的身份页签名。

4. Agent 怎么解析这份身份页

4.1 只做到“可访问”远远不够

很多身份信息其实是公开可访问的,但 Agent 无法低成本使用。比如个人博客首页,Agent 需要处理 HTML 标签、导航栏、脚本、样式,还要从正文中猜测哪句话是身份声明。Username.md 把机器关注的字段集中到 frontmatter,等于给 Agent 划好了重点。

要做到这一点,页面本身要满足几个条件:

  • 通过 HTTPS 提供访问,避免内容被中间人替换。
  • 返回的 Content-Type 至少是 text/plain 或 text/markdown,不要是 application/json 包装过的。
  • 不要使用动态脚本渲染,Agent 不会执行 JavaScript 去等一个异步加载的身份页。
  • 文件体积要小,避免塞入大量无关内容。

4.2 一个简单的 Python 解析示例

下面是一段参考用的 Python 伪代码,作用是把 frontmatter 解析成字典,再根据签名结果决定是否信任。注意这段代码只是演示流程,不是某个官方库的用法:

import hashlib import urllib.request import yaml def fetch_text(url): req = urllib.request.Request(url, headers={"User-Agent": "your-agent"}) with urllib.request.urlopen(req, timeout=10) as resp: return resp.read().decode("utf-8") def parse_frontmatter(text): if not text.startswith("---"): raise ValueError("missing frontmatter") parts = text.split("---", 2) if len(parts) < 3: raise ValueError("frontmatter not closed") return yaml.safe_load(parts[1]), parts[2] def verify_signature(public_key, content_bytes, sig_bytes): # 这里用你选择的签名库实现,核心是: # 1. 用 public_key 校验 sig_bytes 是否与 content_bytes 匹配 # 2. 校验通过返回 True,否则 False pass page_url = "https://example.com/username.md" sig_url = "https://example.com/username.md.sig" content = fetch_text(page_url) sig = fetch_text(sig_url) metadata, body = parse_frontmatter(content) # 注意:验签应该基于原始 content 字节,而不是解析后的 body if verify_signature(metadata["keys"]["pgp"], content.encode("utf-8"), sig.encode("utf-8")): print("signature valid, name:", metadata.get("name")) else: print("signature invalid, discard content")

这段示例说明了一个关键点:验签必须在解析 frontmatter 前后保持一致。最安全的做法是先拿到原始字节,立即验签,验签通过后再做 YAML 解析。不要把解析后的内容拿来验签,因为 YAML 解析可能改变了换行符或字段顺序。

4.3 Agent 应该提取哪些字段并如何存证

Agent 消费身份页时,建议按三层存放结果:

  • 原始层:保存抓取到的 username.md 原文和签名文件,确保后续可以重新验证。
  • 解析层:保存 frontmatter 解析出来的键值对。
  • 信任层:保存验签结果、抓取时间、公钥指纹、以及你决定信任公钥的依据。

信任层是最容易被忽略的。很多 Agent 只保存了“身份信息”,没有保存“为什么我信任这份身份信息”。一旦后续发现公钥有问题,或者页面被篡改,没有证据链就无法追踪当初的判断依据。

4.4 Agent 侧常见解析坑

  • YAML 解析器对 Tab 字符和缩进有严格要求,前端字段中不要混用 Tab 和空格。
  • 公钥指纹如果带空格或冒号,建议统一格式,比如全大写、无分隔符,或者明确标注格式。
  • 时间字段建议统一为 ISO 8601,避免2025/1/15这种有歧义的写法。
  • Agent 抓取时要设置超时和重试,避免某个身份页长期无响应导致整个任务卡死。

5. 托管、发布与持续更新

5.1 托管方案对比

身份页最终要放在一个稳定可达的 URL 上。几种常见托管路线的特点:

托管方式优势需要注意
自有域名 + 静态服务器完全控制,URL 稳定,可配置缓存头需要自己维护服务器和 HTTPS 证书
GitHub Pages免费,支持 HTTPS,流程简单域名和路径受平台策略影响,URL 形式有平台痕迹
Netlify / Vercel支持自定义域名,自动 HTTPS免费额度有限,页面延迟受平台节点影响
IPFS内容寻址,天然防篡改解析速度不稳定,需要固定服务保持内容在线

我不是说哪个一定更好。身份页的核心目标是自己持有,所以域名所有权比托管平台更重要。哪怕你用免费平台托管,只要域名在自己手里,随时可以更换托管商,身份页 URL 不变。

5.2 发布流程应该自动化

手工编辑、手工签名、手工上传,短期跑一两次没问题,长期一定会出错。建议把流程做成脚本或 CI 任务:

  1. 在本地编辑username.md。
  2. 对文件重新签名,生成新的username.md.sig。
  3. 检查 frontmatter 中updated字段是否已更新。
  4. 推送或上传到托管平台。
  5. 用脚本请求公开 URL,做一次验签确认。

第五步非常重要。很多人本地验签没问题,但上传后因为编码转换、末尾换行、平台自动格式化等原因,线上文件的验签失败了。发布后立即做一次“从公开 URL 拉取再验签”的检查,能避免把坏页面暴露给访问者。

5.3 缓存和 CDN 带来的坑

身份页不是热门内容,但如果你用了 CDN,或者托管平台自动做了缓存,就可能会遇到签名文件更新但线上页面还是旧版本的情况。

处理方式有几个方向:

  • 在响应头设置合理的Cache-Control,比如no-cache或者短缓存时间。
  • 签名文件名不要一直叫username.md.sig,可以带版本,如username.md.sig.v2,并在 frontmatter 中引用新名字。
  • Agent 侧抓取时可以带时间戳或随机参数,比如?t=当前时间,强制绕过部分缓存。但最终还是要看服务器是否支持。

记住:身份页追求的是“当前版本可信”,缓存会让旧版本在某个时间段内继续被消费,这不是功能 bug,但你的排查思路里要有这个概念。

6. 边界与常见误判

6.1 验签通过不等于身份真实

这是整个方案里最容易被误读的一点。任何人都可以创建一把密钥,签一个alice.md,页面里写着“我是某公司 CEO”。验签只能确认这个页面确实由这把私钥的作者发布,但不能确认页面里的声明是事实。

要解决“真实身份”问题,需要额外的信任锚:

  • 域名所有权验证:如果alice.md挂在alice.com下,而alice.com的 WHOIS 信息或其他渠道确认为本人持有,域名本身成为信任锚。
  • 社交账户互链:同一把公钥在你的 GitHub、Twitter、邮箱等多处出现,增加身份一致性证据。
  • 第三方背书:由熟悉你的人或机构对你的公钥做签名,形成信任网络。

Agent 在设计信任策略时,必须把“签名有效”和“声明可信”分开打分。签名有效是第一步,来自公钥的信任得分是第二步,域名历史和社会背书是第三步。

6.2 Markdown 内容规范化问题

签名对字节敏感,Markdown 解析对格式敏感,两者叠加后最容易出问题的场景是:

  • 行尾符差异:Windows 的 CRLF 和 Unix 的 LF,在签名内容时会被视为不同字节。
  • 末尾是否有换行:很多编辑器会在文件尾部自动加换行,如果签名时没有,线上验证时可能失败。
  • UTF-8 BOM:有些编辑器会写入 BOM,导致文件开头多出三个字节。
  • 前端字段大小写:Agent 解析时如果区分大小写,而实际字段用了不同大小写,就会读取失败。

建议在签名流程里加入一个规范化步骤:固定使用 UTF-8 无 BOM、LF 行尾、末尾一个换行。Agent 侧抓取内容后不要做任何额外转换,直接用原始字节验签。

6.3 多密钥和权限管理

身份签名密钥不应该和日常开发密钥混用。我见过有人把身份页签名用的私钥直接放在服务器 SSH 目录里,一旦服务器被入侵,整个身份信任链都完蛋。

比较稳妥的做法是:

  • 身份密钥单独生成,单独管理。
  • 私钥保存在离线加密环境中,只在签名时临时给到进程。
  • 页面中可以同时列出多把公钥:一把主身份密钥、一把子密钥或备用密钥。
  • 如果某把密钥泄露,立即在页面上声明失效,并用另一把正常密钥对新内容签名。

6.4 跨平台身份关联的预期管理

Username.md 是一个很好的起点,但它不是万能的。它解决的是“我有一个稳定、可验证的身份页”,但不解决“所有平台自动认可这个身份页”。GitHub 不会因为你有 username.md 就自动认证你的账号,邮件服务商也不会自动把公钥绑到你的邮箱。

要把这个身份页变成多方认可的凭证,你还需要在常用平台上公开这个页面的 URL,并尽可能让各平台信息指向同一把公钥。这个过程有点像滚雪球:身份页是核心,公钥是锚点,平台账号是信任来源,三者互相印证,信任网络才慢慢建立起来。

7. 踩坑之后我建议的排查顺序

7.1 验签失败先查什么

验签失败是最常见的身份页问题。不要一上来就怀疑工具坏了,按这个顺序排查:

  1. 看原始字节:把线上文件下载后和本地文件做一次sha256sum对比,确认是否一致。
  2. 看行尾和编码:用工具检查是不是 CRLF、是不是带了 BOM。
  3. 看签名文件是否过期或下载不完整:有的服务器会截断或者给签名文件加错误 headers。
  4. 看公钥指纹:确认你导入的公钥确实是对应私钥生成的公钥,而不是一个同样名字但内容不同的钥匙。
  5. 看时间:如果签名引用了时间戳,确认系统时间和签名生成时间没有巨大偏差。

7.2 Agent 解析不到字段先查什么

验签成功但解析不出内容,优先级是:

  1. 确认 frontmatter 是否真的被解析:很多 Markdown 渲染器会自动忽略 frontmatter,但你的 YAML 解析器未必认识。
  2. 确认缩进:YAML 对嵌套字段的缩进敏感,字段层级错一位就会解析失败。
  3. 确认字段名大小写:你自定义的字段是 camelCase 还是 snake_case,Agent 侧要统一。
  4. 确认是否有不可见字符:比如零宽空格,某些编辑器会自动插入,导致字段 key 匹配不上。

7.3 一份排查对照表

现象常见原因优先检查点
验签失败文件字节不一致对比线上和本地的 sha256
验签失败行尾符或 BOM 问题检查文件编码和 line ending
验签失败公钥指纹不匹配重新核对 keys 字段中的 fingerprint
解析不出 namefrontmatter 缩进错误检查嵌套字段的缩进层级
解析不出 keys字段名大小写不一致统一字段命名规范
身份页 404文件名大小写错误确认 URL 中 username 大小写
页面被缓存CDN 缓存未过期设置 Cache-Control 或加版本参数
更新后线上旧内容发布流程没有验签增加发布后自动验签步骤

这张表本质上是把“先看什么”给固化下来。排查时不要跳步,先文件层,再解析层,再信任层。

最后留几句经验

我把整个方案在本地环境完整跑了一遍之后,最大的感受是:Username.md 的价值不在于做一个漂亮的页面,而在于让“身份信息”本身具备可验证、可迁移、可被 Agent 消费的属性。

个人建议的落地顺序是:

  1. 先用一个简单的 username.md 把自己的名字、公钥和联系方式写进去。
  2. 用熟悉的签名工具跑通签名和验证的闭环。
  3. 挂到自己的域名或静态托管上,确认公开 URL 可访问。
  4. 再考虑要不要做一个 Agent 解析小工具,定期抓取并验签。
  5. 最后才是把身份页和社交账号互链,逐步建立外部信任。

踩过几次坑之后我更确定一件事:很多问题不是工具能力不够,而是文件规范化、公钥管理和发布流程没有处理好。只要把这三件事理顺,Username.md 这套思路就能稳定跑起来。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询