2026-09-23·by Sijie Wang#standmeet#architecture#design

owner-keypair-auth

所有者密钥对认证(Ed25519 Sigv1)

父级:access-control

owner MCP handle/mcp/*)对每一个 HTTP 请求都要求有效的 Sigv1 签名——旧的 Bearer PAT 在密钥对认证取代 PAT 时被移除(routes/mcphandle/server.go)。

范围界定: Sigv1 只守卫对外的服务 handle(StandMeet 作为 MCP 服务端as-mcp-facade)。对内的能力平面(我们的 agent 把 mcp-servers/ 当作 host 来消费)是进程内本地的——stdio 子进程 + capsocket——从不经过这层认证(confusables)。

密钥生命周期

密钥对在服务端生成ed25519.GenerateKeybackend/internal/owner/usecase/keypairs.go);私钥 PEM 只在 POST /api/admin/keypairs 的响应中返回这一次,此后不再存储——只有公钥持久化在 owner_keypairs 表中。领域层注释写着:"the private key never enters the domain (owner keeps the PEM)"(owner/entity/keypair.go:2)。管理端的增删改查走的是普通的 owner session cookie;列表接口只返回元数据(现在含这把 key 最后在哪里用过——last_used_ip + last_used_user_agent,2026-09-05 上线,169a51d79,migration 2026-09-06-keypair-last-used-meta.sql);DELETE /{key_id} 是硬删除,等同于吊销(没有状态列)。

线上格式

Authorization: Sigv1 keyId=<X>,ts=<unix-seconds>,nonce=<uuid>,sig=<base64>

被签名的消息是一个固定的 challenge,而不是请求本身:

"standmeet-sigv1" + "\n" + keyId + "\n" + ts + "\n" + nonce

验证流程

前缀与四个字段齐全 → ts 落在 ±5 分钟的时钟偏差范围内 → 按 keyId 查公钥(查不到 → 401,且不泄露该 key 是否存在)→ ed25519.Verify → nonce 首见检查(Redis SetNXsigv1nonce:<keyId>:<nonce>,TTL = 2×偏差窗;Redis 挂了则 fail-open)→ 尽力而为地更新 last_used_at + last_used_ip + last_used_user_agent → ownerID 被注入 ctx 并传播给 MCP 的工具处理器(keypairs.go:176-302)。

安全模型

Warning

签名并不覆盖请求本身。 challenge 绑定了身份 + 时间戳 + 一次性 nonce——不包含方法、路径或请求体。自 2026-07-04(0fa5177e1)起,被截获的 header 重放即死(nonce 在首次接受时就在 Redis 里烧掉),但它认证的仍是调用者,而不是每一条消息:窗口内的攻击者只有握有私钥才能给另一个 /mcp/* 请求配上新签的 header。nonce 检查在 Redis 故障时 fail open——对 owner 面可接受,也正是 embed 那条对应路径要 fail closed 的原因。在当前的威胁模型下(单一 owner、假定有 TLS)这是可以接受的;剩下的升级路径是对 method + path + body-hash 签名。(keypairs.go:4 的文件头注释仍写着"no nonce table"——下面的代码并非如此;以代码为准。)

类视图

Setup token(一个独立的子系统)

setup token 是另一回事——它是首次启动时对实例的认领凭证(启动时打印 + 写入 /srv/first-run.txt,sha256 后存入 instance_settings,被 POST /api/admin/claim 消费一次,由此创建 owner)。它负责引导出 owner 账号;密钥对是之后才通过已认证的 admin API 生成的(confusables)。

交叉链接:trusted-identity-via-metaacl-and-quota-granularity

about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →

owner-keypair-auth