03 · 可信:签名与命名
本章讲
dir怎么把“这条记录是谁发的、有没有被改、名字归不归他”做成可验证的。核心机制是 referrer 附件:可信信息挂在记录旁边,而不是塞进记录里。
1. 这章要解决的小问题
01 章 的 CID 已经保证了“内容没被改”(改了 CID 就变)。但还有两个问题它管不了:
- 是谁发的? —— CID 只证明内容完整,不证明来源。
- 名字归不归他? —— 记录里写
name: cisco.com/agent,凭什么相信它真是 cisco.com 发的?
这两件事靠签名和域名所有权校验解决,且都设计成叠加层——不影响存储/发现主路径。
2. 核心机制:referrer(附件)
直觉: 记录本身不可变(CID 锁死),那签名往哪放?答案是放在旁边——一个指向记录 CID 的“附件对象”,叫 referrer。
referrer 的结构(proto/agntcy/dir/core/v1/record.proto:68 RecordReferrer):带一个 type(如 "agntcy.dir.sign.v1.Signature")、一个指向主记录的 record_ref、和实际数据 data。OCI 仓库天然支持这种“artifact 引用 artifact”的关系。
为什么这么设计: 同一条记录可以挂多个 referrer——多个签名者、一个公钥附件、未来还能挂别的证明——而主记录的 CID 永远不变。可信是“往记录上贴标签”,不是“改记录”。
3. 主线一:签名(Sign)
签名是客户端侧操作(proto 注释:“This is a client-side service and is not available on the server”,proto/agntcy/dir/sign/v1/sign_service.proto)。流程在 client/sign.go:19(Client.Sign):
下图怎么读:从左到右,被签的对象是 CID 字符串本身,不是整条记录。
record (已有 CID)
│
▼
对 CID 字符串签名 ──┬── Key 方式: cosign.SignBlobWithKey(CID, privKey)
(cosign) └── OIDC 方式: cosign.SignBlobWithOIDC(CID, oidc) ← keyless
│
▼
得到 {signature, publicKey}
│
▼
pushReferrersToStore():
• PushReferrer(type=PublicKey, ...) ← 公钥作为附件
• PushReferrer(type=Signature, ...) ← 签名作为附件
关键点:被签的载荷是 req.GetRecordRef().GetCid()(client/sign.go:38/:41),即对“内容指纹”签名。因为 CID 已经唯一锁定了内容,对 CID 签名 == 对内容签名,但载荷只有几十字节。
两种 provider:
| 方式 | 怎么用 | 适合 |
|---|---|---|
| Key | PEM 私钥(可带 passphrase) | 自管密钥的私有场景 |
| OIDC(keyless) | 走 Sigstore Fulcio/Rekor/OIDC,无需长期持有私钥 | 公开可审计的供应链签名 |
OIDC 默认指向 Sigstore 公共服务(fulcio.sigstore.dev、rekor.sigstore.dev 等,proto/.../sign_service.proto 的 SignOptionsOIDC)。可设 skip_tlog 跳过透明日志(私有签名,但就无法对 Rekor 验证)。
4. 服务端怎么接住签名 referrer
签名/公钥通过 StoreService.PushReferrer 推到服务端(server/controller/store.go:254 pushReferrer)。这里有两个联动副作用值得注意:
- 收到 Signature 类型 referrer →
s.db.SetRecordSigned(recordCID)标记该记录“已签名”,好让后台 name 任务知道要去校验名字(server/controller/store.go:291-298)。 - 收到 Signature 或 PublicKey →
InvalidateSignatureVerificationsForRecord清掉缓存的签名验证结果,让 reconciler 重新验证、把新签名者(比如后补的公钥签名者)都算进去(:302-308)。
5. 主线二:域名所有权校验(Naming)
问题: 记录声称 name: cisco.com/agent,怎么确认发布者真控制 cisco.com?
机制: 后台 reconciler 的 name 任务周期性校验。流程(reconciler/README.md “Name Task”):
- 查数据库里“已签名、带可验证名字(
http:///https://前缀)、且校验缺失/过期”的记录。这个“可验证名字”的硬性前缀过滤就发生在这一步的 SQL 里(server/database/gorm/naming.go:133GetRecordsNeedingVerification:name LIKE 'http://%' OR LIKE 'https://%')。 - 取记录名字 + 挂在记录上的公钥。
- 去名字对应的域名拉 JWKS(well-known 端点),核对公钥归属,确认名字所有权。
- 把校验结果缓存(带 TTL)。
校验是自动的、后台的(naming proto 注释:“Verification is performed automatically by the backend scheduler”,proto/agntcy/dir/naming/v1/naming_service.proto:11)。well-known 抓取器在服务端注入(server/server.go:300-304 wellknown.NewFetcher),校验 TTL 来自配置(server/server.go:318 Naming.GetTTL)。
客户端只读结果:NamingService.GetVerificationInfo 返回 verified 布尔 + 详情(proto/.../naming_service.proto:46)。