@claudewerk/mutual-key-auth

中文文档
0.2.1发布于 2026/10/27MITTypeScriptES Module零依赖近 30 天下载 1910

互换,独立可撤销的无记名钥匙认证,并附有质疑响应方向证明和下级约束.

中文使用文档
文档目录17 节
  1. 为什么存在(威胁模式)
  2. 已知取舍( 读此)
  3. 使用量
  4. 签字方法 ed25519-sig ) -- 更喜欢这个你可以
  5. 配对入场券——每对对等者可复制一个字符串
  6. 薄荷, 储存散列, 校验
  7. 相互握手( asAuthProvider ——接通可靠接通 lib)
  8. 方向验证原始( 下等, 下等)
  9. 范围 - > 动词检查( 纯)
  10. 撤销旋转( R)
  11. API 密码
  12. 带外提供密钥
  13. 衍生(委托)密钥
  14. KeyRecord
  15. 日志
  16. 安全保证和非目标
  17. 许可证

双键原子

相互、可独立撤销 无记名钥匙认证 与 挑战反应方向证明 财务报告和审定财务报表 下级绑定独立和 无依赖 - node:crypto 仅Bun WebCrypto, pino 一个可选的对等点.

它对WebSockets,队列或任何更大的系统一无所知. 它薄荷和 验证密钥,制作并检查方向证明. 传送器插入 作为 AuthProvider (duck-typed -- 两种方式都不导入).

bun add @claudewerk/mutual-key-auth      # or: npm i @claudewerk/mutual-key-auth
bun test                      # 58 tests, all attack cases included

为什么存在(威胁模式)

连接上的两个同伴 都想不断独立地证明, 对方仍然持有有效的证书 -- 没有人降低 谈判参数 中手摇。 每个机制都为特定攻击辩护:

机制防御
存储的密钥 散开 (SHA-256),一次显示原始DB/唱片被窃并不能揭示出原始的无记名密钥(没有预想). 它不会停止握手作假:所储存的散列是防向的HMAC密钥,因此被窃记录可以冒充同位素. 警卫记录,如私钥;固定为不对称证明(RFC 0004§9.1/§10)
常时 hash + 证明比较泄露秘密字节的时空侧道
过期 每个键泄露的密钥仅在到期前有用
独立撤销 (按键,任意一边)折中一个对等方的密钥不会迫使另一个完全重键
按键旋转 (两个活键重叠)在不硬切换/ 丢弃连接的情况下滚动密钥
加拿大 单用途非ce重放被捕获的证明(nonce为一发;被烧毁的nonce不能被再用)
诺斯 计时器无限挑战有效性; 缩小重播窗口
方向证明 =: HMAC(transcript, keySecret)证明拥有 不把钥匙放电线上; 将证据绑定在一个证明角色上
角色绝对 记录initiatorHello+ 键responderHello 固定顺序, proverRole 页:1两端都用零视角杂耍来计算字节-同声记录 - 没有我的/同声互换的脚枪
缩写绑定 两个相框的花序 + 谈判的跨层拖轮 {version, features, and every negotiation slot's chosen name}每层都有降级攻击 - 篡改任何谈判参数(运输) 或者说 app-level:编码,功能集,认证方法)更改了笔录,因此证明不再被验证(SSH exchange-hash / TLS-Fined property)
transcriptHash 绑定 (用两个原始的广告打招呼框)MITM在谈判前根据公布的能力进行编辑 -- -- 认证全文,而不仅仅是谈判结果
connId 绑定交叉连接重放抓取的证据到不同的连接
长度,固定数量记录凝聚-凝聚攻击+ 添加/移动-场 篡改地界
域分隔标签 + 记录中的格式标签重新使用证据 HMAC 作为通用签名预告
范围 - > 动词 矩阵检查过于宽泛的授权;一个关键只是它所赋予的范围
编辑日志意外地记录原始密钥、散列或证据
ed25519-sig 方法:记录持有公用钥匙,证据是同一笔录上的Ed25519签名(RFC0004§10)校验-存储盗窃:被窃记录验证了证据但不能制作一个,所以不能冒充对等者-关闭下方的HMAC取舍
签名配对票 (单位:千美元)brgt1_..., Ed25519,载在标有标记的罐装有效载荷上,过期,为4KiB盖)URL 或 公钥在主机之间复制票时被交换 -- -- 编辑后卡失败 bad-signature
对格式错误的输入进行失败的闭合握手 (非字符串 / 过大的非ce 或 证明 - > malformed-auth-message; 加密层本身的拒绝被捕获)远程相撞:在载体的认证交换中抛出一个会终止这一过程,所以一个可以制造的对等点 prove() 用一个短的非ce来扔, 在认证前会杀死一个听众

已知取舍( 读此)

证据是 对称的 HMAC 由密钥在后端的密钥键入 (单位:千美元)SHA-256(rawKey) == record.hash) (中文(简体) ). 证明者从它的生钥匙中得来 核查者已将其记录在案; 原始密钥从不移动编辑 权衡:偷取所存储记录的攻击者可以 伪造方向证明 (虽然没有通过生钥匙核查 -- -- 这仍然需要一个预想). 这是 内在的对称 HMAC 有散列存储。 缓解:短期限

  • 快速独立撤销,两者都建在. 如果你需要证据才能活下来 恢复妥协,使用 ed25519-sig 方法(下一节):相同 握手,同样的下级绑定,记录只持有公用钥匙.

使用量

签字方法ed25519-sig) -- 更喜欢这个你可以

import { mintSigningKey, recordForPublicKey, publicKeyOf, asSigAuthProvider } from "@claudewerk/mutual-key-auth";

const me = mintSigningKey({ peerId: "hostA" });   // { secretKey, record }
// ship me.record (or just publicKeyOf(me.secretKey)) to the peer; it holds no secret
const peerRecord = recordForPublicKey(peerPublicKey, { peerId: "hostB", scopes: ["send"] });

const provider = asSigAuthProvider({ mySecretKey: me.secretKey, peerRecord }); // method "ed25519-sig"

secretKey 实值 brg-ed25519_<base64url 32-byte seed>: 储存像任何 私钥. 握手、发出信息以及拒绝的原因是: asAuthProvider;只有原始证据不同. 一个听众从 mutual-key 两种方法同时提供(bun-liable-ws) authByMethod) (中文(简体) ).

配对入场券——每对对等者可复制一个字符串

import { mintTicket, parseTicket } from "@claudewerk/mutual-key-auth";

// host B prints its card; host A pastes it
const card = mintTicket(B.secretKey, { peerId: "hostB", urls: ["wss://b.example/bridge"], expiresAt });
const got = parseTicket(card, { scopes: ["send"] }); // { ok, ticket: { peerId, publicKey, urls, ... }, record }
// got.record is A's key record for B; got.ticket.urls is where to dial

票是自签的联络卡: peerId,Ed25519 公用钥匙,拨打URL, 发行时间和可选过期。 签名在中途捕获任何编辑 不证明密钥属于你认为的人 -- -- 信任是频道 你收到它过来。 配对是一张票 从来没有秘密 移动。 范围是VERIFIER的设定(parseTicket(..., { scopes })从不 一张卡片上写的 拒绝: malformed, (中文(简体) ), unsupported-version, (中文(简体) ), bad-signature, (中文(简体) ), expired。 。 。 .

薄荷, 储存散列, 校验

import { createAuthProvider } from "@claudewerk/mutual-key-auth";

const auth = createAuthProvider({ logger: "silent", prefix: "brg" });

// Listener mints a key for a peer. Show `key` ONCE; persist only `record`.
const { key, record } = auth.mintKey({
  peerId: "dialer-1",
  scopes: ["read", "write"],
  expiresAt: Date.now() + 86_400_000, // 24h
});
// key    -> "brg_Xf9...b64url..."   (give to the peer, never stored/logged)
// record -> { id, peerId, scopes, expiresAt, status:"active", hash, ... }

// Later, verify a presented key against the stored record:
const r = auth.verifyKey(presentedKey, record);
// { ok: true, peerId, keyId } | { ok: false, reason: "expired"|"revoked"|"key_mismatch"|... }

相互握手(asAuthProvider ——接通可靠接通 lib)

asAuthProvider 符合 没错 页:1 AuthProvider 约一个可靠的 - WebSocket 库( R)bun-liable-ws (可信赖的bun-liable-ws) (英语)-形状)驱动器 - 不 适配器,无胶. 它回来了 { method, start(ctx), step(ctx, incoming) } 财务报告和审定财务报表 运行三个消息的相互交换 在证明原始。 把它作为电线 工厂auth: () => asAuthProvider({...}))——每次接通一次握手.

import { asAuthProvider } from "@claudewerk/mutual-key-auth";

// Mutual keys: each side holds its OWN raw key; each holds the OTHER's record.
const dialer   = asAuthProvider({ myKey: myRawKey,  peerRecord: peerRecordForListener });
const listener = asAuthProvider({ myKey: myRawKey2, peerRecord: peerRecordForDialer });

连接库各递 start编号 :step 调用 a 新鲜 AuthContext (它每次重建它),所以 所有握手状态 住在提供者 关闭, 永远不要在 ctx 上。你什么也得不到 -- -- 图书馆提供:

AuthContext = { role: "dialer"|"listener", negotiated, connId,
                localCapabilities, remoteCapabilities, transcriptHash, localPeerId }
AuthStep    = { done:false, send } | { done:true, ok:true, send? }
            | { done:true, ok:false, reason, send? }
// The library drives it -- shown here manually with a fresh ctx per call:
const d0 = dialer.start(dialerCtx());        // -> { done:false, send:{type:"challenge"} }
listener.start(listenerCtx());               // -> null (listener waits)
const l1 = listener.step(listenerCtx(), d0.send);   // -> challenge-response
const d1 = dialer.step(dialerCtx(),   l1.send);     // -> { done:true, ok:true, send:response }
const l2 = listener.step(listenerCtx(), d1.send);   // -> { done:true, ok:true }
// d1.ok && l2.ok === mutual success. A tampered negotiated/connId/transcriptHash/
// capability aborts it with { ok:false, reason:"bad-proof" }.

每个角色的证明都具有约束力: H(initiatorHello) + 键 H(responderHello) (从) local编号 :remoteCapabilities页:1 negotiated tuple(版本、特征和 每个谈判时间段的选定名称, connId,以及 transcriptHash - 所以任何跨层降级或MITM 能力编辑断开握手.

方向验证原始( 下等, 下等)

握手就是建立在这个基础上的. 这个 核查者 签发非ce 验证 返回共享上的 HMAC 角色绝对 记录 两端都通过 相同的元素——没有视角杂耍.

import { PROVER_ROLE } from "@claudewerk/mutual-key-auth";

const parts = {
  proverRole: PROVER_ROLE.INITIATOR,     // enum: who is proving (not a direction string)
  initiatorHello,                        // FIXED order: initiator first,
  responderHello,                        //   then responder -- same on both ends
  tuple: { version: 1, features: ["ordered"], auth: "mutual-key", e2eSeal: "none", e2eSign: "none", codec: "json" },
  connId,                                // binds connection identity (optional)
  transcriptHash,                        // binds the full advertised-capability transcript (optional)
};

const nonce = auth.newNonce();                                      // verifier issues
const proof = auth.makeProof(proverRawKey, { nonce, transcriptParts: parts }); // prover (key never leaves)
const ok = auth.verifyProof(proof, {                               // verifier: consumes nonce, checks
  nonce, transcriptParts: parts, keyRecordForProver: proverRecord //   revocation/expiry, then HMAC
});
// ok === true only if the key is valid AND no bound parameter (any layer) was tampered

一个非ce=一次尝试. verifyProof 在加密前消耗非ce 检查,所以一个失败的证据会烧掉它 -- 用新鲜的来重试挑战 newNonce(),从不重发相同的非ce。 (悬挂在 DESIGN-FEEDBACK.md #3. ) (中文(简体) )

范围 - > 动词检查( 纯)

import { scopeAllows } from "@claudewerk/mutual-key-auth";

const matrix = { read: ["get", "list"], write: ["put", "delete"], admin: ["*"] };
scopeAllows(["read"], "get", matrix);    // true
scopeAllows(["read"], "delete", matrix); // false
scopeAllows(["admin"], "anything", matrix); // true  ("*" grants all)

撤销旋转( R)

const revoked = auth.revokeKey(record); // returns a revoked copy; verify* now rejects it
// Rotation: mint a second key before expiring the first -- both verify while active.

API 密码

// The handshake AuthProvider a reliable-connection lib drives (challenge/response):
asAuthProvider({ crypto?, myKey, peerRecord, method?, domainSepLabel?, now? })
  -> { method, start(ctx) -> AuthStep|null, step(ctx, incoming) -> AuthStep }
  ctx      = { role: "dialer"|"listener", negotiated, connId,
               localCapabilities, remoteCapabilities, transcriptHash }  // READ-ONLY, rebuilt each call
  AuthStep = { done:false, send } | { done:true, ok:true, send? }
           | { done:true, ok:false, reason, send? }
  // State lives in the closure, NOT on ctx -- create one provider per connection.

// The same handshake with Ed25519 signature proofs (RFC 0004 §10):
asSigAuthProvider({ mySecretKey, peerRecord, method?, domainSepLabel?, now? })  // method "ed25519-sig"
mintSigningKey({ peerId, scopes?, expiresAt?, prefix?, rateLimit?, allowlist? }) -> { secretKey, record }
recordForPublicKey(publicKey, { peerId, scopes?, expiresAt?, ... }) -> record
publicKeyOf(secretKey) -> string   // base64url raw public key
makeSigProof(secretKey, { nonce, transcriptParts }) -> string
verifySigProof(proof, { nonce, transcriptParts, keyRecordForProver }) -> boolean

// Pairing tickets (self-signed contact cards for ed25519-sig peers):
mintTicket(secretKey, { peerId, urls, expiresAt?, now? }) -> "brgt1_..."
parseTicket(text, { scopes?, now? }) -> { ok: true, ticket, record } | { ok: false, reason }

// The stateful key/nonce/proof provider (crypto operations, not a state machine):
createAuthProvider({ logger?, prefix?, nonceTtlMs?, nonceBytes?, now? }) -> {
  mintKey({ peerId, scopes?, expiresAt?, prefix?, rateLimit?, allowlist? }) -> { key, record }
  // recordForKey(key, { peerId, ... }) -> record   (top-level export; for a key you already hold)
  verifyKey(presented, record) -> { ok, reason?, peerId?, keyId? }
  newNonce() -> string
  makeProof(myIssuedRawKey, { nonce, transcriptParts }) -> string
  verifyProof(proof, { nonce, transcriptParts, keyRecordForProver }) -> boolean
  revokeKey(record) -> record
  scopeAllows(scopes, verb, matrix) -> boolean
}

// transcriptParts (role-absolute; tuple is any canonicalizable object):
{ proverRole: PROVER_ROLE.INITIATOR|RESPONDER, initiatorHello, responderHello,
  tuple,              // e.g. WS Negotiated { version, features, auth, e2eSeal, e2eSign, codec }
  connId?, transcriptHash?, domainSepLabel? }

纯函数也直接导出( N)asAuthProvider, (中文(简体) ), mintKey, (中文(简体) ), recordForKey, (中文(简体) ), deriveKey, (中文(简体) ), verifyKey, (中文(简体) ), makeProof, (中文(简体) ), verifyProof, (中文(简体) ), buildTranscript, (中文(简体) ), mintSigningKey, (中文(简体) ), recordForPublicKey, (中文(简体) ), publicKeyOf, (中文(简体) ), makeSigProof, (中文(简体) ), verifySigProof, (中文(简体) ), asSigAuthProvider, (中文(简体) ), SIG_METHOD, (中文(简体) ), mintTicket, (中文(简体) ), parseTicket, (中文(简体) ), PROVER_ROLE, (中文(简体) ), newNonce, (中文(简体) ), NonceStore, (中文(简体) ), scopeAllows, (中文(简体) ), createLogger, (中文(简体) ), redact)用而无国供.

带外提供密钥

mintKey 生成一个密钥,并手拿回记录。 当钥匙 已经存在 - 它来自一个配置文件, 一个秘密商店, 或者一个固定的二 进程共享 -- -- 从中获取记录:

import { recordForKey } from "@claudewerk/mutual-key-auth";

const peerRecord = recordForKey(process.env.PEER_KEY, { peerId: "rust-client" });

mintKey 实值 generate + recordForKey,因此两者生成相同的记录用于 同样的钥匙。 这就是让一个TS主机和一个单独的客户端程序达成一致的原因 对方的记录没有相互传递记录: 启动时对等方的生密钥,获得记录,只保留散列.

衍生(委托)密钥

一个长寿的父键可以支持许多短寿的同龄人,没有 薄荷服务,没有密钥存储. 父母的持有者每个子女 把它交给它产生的过程; 核查的一面,它也是 持有父母,从同龄人声称的科目中重新选定同一个孩子:

import { deriveKey, recordForKey } from "@claudewerk/mutual-key-auth";

// spawner: one child key per conversation, passed in the child's env
const childKey = deriveKey(parentKey, conversationId);

// verifier: pick the record by the peer id claimed in the hello
const peerRecordFor = (peerId) =>
  peerId.startsWith("host/") ? recordForKey(deriveKey(parentKey, peerId.slice(5)), { peerId }) : undefined;

deriveKey 实值 HMAC-SHA256(parent, "mutual-key-auth/derive/v1" || 0x00 || subject) 作为 <prefix>_<base64url>儿童不透露其父母或 (a) 父母的兄弟姐妹; 谁拿着 因此,父母是家庭的秘密.

KeyRecord

// mutual-key
{ id, peerId, scopes[], rateLimit?, allowlist?, expiresAt, status: "active"|"revoked",
  hash, prefix, createdAt }
// ed25519-sig: no secret at all
{ id, peerId, scopes[], rateLimit?, allowlist?, expiresAt, status: "active"|"revoked",
  alg: "ed25519", publicKey, createdAt }

id 是一个短而安全的到日志的手柄(散列的前12个六分轴). hash 这是 满 SHA-256(rawKey) - 在休息时视之为敏感(见上文权衡)。 这个 显示原始密钥 仅限 内 mintKey返回,从未坚持或记录.

日志

可注入、结构化和 编辑。通过 logger数字 : "silent" (违约), "console", (中文(简体) ), "pino" (懒惰的可选对等点 -- -- 仅需要) 或任何鸭子型的 { info, warn, error, debug } 对象。 每个 后端是包着的 key编号 :secret编号 :proof编号 :token 被删除 hash 实值 被蒙到前缀上——一个生键即使呼叫者通过一个也不能到达日志.

安全保证和非目标

  • 仅限CSPRNG - 说吧 node:crypto randomBytes 编号 : getRandomValues。测试 声称没有源文件调用 Math.random(。 。 。 .
  • 常时 比较关键和证据核查.
  • HMAC的证据是 单一用途 -- 域分隔和格式标记, 不 一个通用签名神谕。 不得再用关键材料作别用.
  • 没有 运输、会话管理或费率限制(rateLimit编号 :allowlist 被记录在记录上,供打电话者强制执行. 没有 不对称 -- 来看权衡和 DESIGN-FEEDBACK.md。 。 。 .

许可证

麻省理工学院.

版本与发布记录

@claudewerk/mutual-key-auth 共发布 3 个版本,首次发布于 2026/09/28;过去 12 个月发布了 3 个版本。

版本发布日期距今
0.2.1latest2026/10/2713 天前
0.2.02026/09/281 个月前
0.1.02026/09/281 个月前

关于 @claudewerk/mutual-key-auth 的常见问题

@claudewerk/mutual-key-auth 是什么?

互换,独立可撤销的无记名钥匙认证,并附有质疑响应方向证明和下级约束.

如何安装 @claudewerk/mutual-key-auth?

在项目目录运行 npm install @claudewerk/mutual-key-auth,或使用 pnpm add / yarn add / bun add。

@claudewerk/mutual-key-auth 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@claudewerk/mutual-key-auth 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@claudewerk/mutual-key-auth 可以商用吗?

@claudewerk/mutual-key-auth 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@claudewerk/mutual-key-auth 的下载量有多少?

根据 npm 官方下载统计,@claudewerk/mutual-key-auth 在 2026/10/10 – 2026/11/08 这 30 天内被下载 1,910 次,日均约 64 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@claudewerk/mutual-key-auth 还在维护吗?

最新版本 0.2.1 发布于 2026/10/27(13 天前);至今共发布 3 个版本,其中过去 12 个月发布了 3 个;首次发布于 2026/09/28。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。