[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fhdvmYaj-kU-AlF79v4Nn6g2tKFBs5R_E_8Y0wmTrrx4":3,"$fW7BAB5BkhrpFei-euf609NeK4ZvjPf9T1fzgXJlLNns":18,"$fM9xEztxptNz1QWfxDH2EgHwfiPvQCmLklAmKMSuGi-M":74,"$fJAngGPN2ZoweBAUNUMveHW9fX-PBt_OThOGUnXXFK7w":105},{"success":4,"data":5},true,{"siteTitle":6,"siteDescription":7,"siteSubtitle":8,"siteFaviconUrl":9,"siteLogoUrl":10,"footerText":11,"footerLinks":12,"socialLinks":13,"postsPerPage":14,"themeName":15,"navColor":16,"navTextColor":17},"Hyaika Blog","A personal blog powered by Hyaika","Penguin is all you need","🐧","\u002Fapi\u002Fmedia\u002Favatar_a0b54615","致三千年前的你",[],[],10,"kratos","#9147eb","#ffffff",{"success":4,"data":19},[20,27,33,39,45,51,56,62,68],{"id":21,"name":22,"slug":23,"description":24,"color":25,"postCount":26},"9ca4490e-c5a6-4b61-945c-4db21d224507","设计","design","UI\u002FUX 设计与创意",null,44,{"id":28,"name":29,"slug":30,"description":31,"color":25,"postCount":32},"a102062c-2d51-415b-bc5c-5b89b36f6e3f","动漫","anime","动漫点评与推荐",25,{"id":34,"name":35,"slug":36,"description":37,"color":25,"postCount":38},"b14ff5c7-a673-4cb1-a9e5-c785069b2938","生活","life","生活随笔与日常分享",46,{"id":40,"name":41,"slug":42,"description":43,"color":25,"postCount":44},"cat_ccf18b24e2c04191","社会观察","social-observation","社会观察、教育、文化对比",26,{"id":46,"name":47,"slug":48,"description":49,"color":25,"postCount":50},"cat_news_roundup","新闻杂烩","news-roundup","每日新闻汇总，覆盖科技、二次元、游戏、音乐等领域",98,{"id":52,"name":53,"slug":54,"description":25,"color":25,"postCount":55},"cat_science","科学","science",80,{"id":57,"name":58,"slug":59,"description":60,"color":25,"postCount":61},"e6b59e04-130e-4da0-851f-64042040f4f6","技术","tech","技术教程与开发经验",219,{"id":63,"name":64,"slug":65,"description":66,"color":25,"postCount":67},"cat_09e5464f1b304aa8","情感八卦","gossip","情感话题与八卦杂谈",1,{"id":69,"name":70,"slug":71,"description":72,"color":25,"postCount":73},"cat_b22f7ce5ece64985","经济","economy","经济分析与商业观察",34,{"success":4,"data":75},{"id":76,"title":77,"slug":78,"content":79,"summary":80,"coverUrl":81,"readingTime":14,"viewCount":82,"loveCount":83,"publishedAt":84,"createdAt":84,"author":85,"coverSource":88,"showCoverInArticle":4,"categories":89,"tags":91,"commentCount":67,"liked":104},"c882f594-a842-41ae-8466-198217b51cea","passkey 不是陷阱，但「陷阱」也不是无的放矢","passkey-attestation-trap-or-not","# passkey 不是陷阱，但「陷阱」也不是无的放矢\n\n## 目录\n\n- **一场辩论，两篇博文**\n- **attestation 到底是什么：一个空对象，和一个证书链**\n- **「陷阱」论：有能力的风险，正在等待时机**\n- **反方：你的 passkey 根本没有 attest**\n- **YubiKey 的悖论：推荐你逃离的东西，正是它唯一在做的事**\n- **我在服务器上翻了一遍 FIDO 的底裤**\n- **两方都对。问题是你看哪边**\n\n---\n\n## 一场辩论，两篇博文\n\n上周，一篇标题为《Passkeys are a trap. Do not use them.》的长文在 HN 爬到了 368 分。作者写给「非技术网民」，核心论点：passkey 背后藏着一个叫 remote attestation 的机制，能让网站逐步收紧到你只能用「批准的软件、批准的系统、批准的固件」登录——你的 Netflix 密码再也不能分享了，你的 LineageOS 可能连不上网了，你的 uBlock Origin 可能被浏览器封杀了。\n\n三天后，安全研究员 Scott Helme 写了一篇直接回应：《Passkeys Are Not a Trap》。他本人写过 passkey 开源库、部署过 passkey 登录、做过安全审计。他的回应很简短，但每个论点都带技术细节。\n\n我两篇都读了。有意思的是，它们没有在事实上打架——双方对 WebAuthn 标准的描述基本一致。真正的分歧在于：**一个在讨论「标准允许什么」，另一个在讨论「现实中发生了什么」。**\n\n---\n\n## attestation 到底是什么：一个空对象，和一个证书链\n\n先把最难的部分拆开讲。\n\n你注册 passkey 时，浏览器和服务器之间走的是 WebAuthn 协议。最终服务器收到的注册数据里有一项叫 `attestation`，翻译过来是「证明」——证明这个 passkey 是用什么设备生成的。\n\n但这里有个关键细节，双方的作者都知道，但「陷阱」一文的读者未必意识到：**attestation 有多种格式，绝大多数消费者 passkey 返回的是 `fmt: none`。**\n\n我用 Python 把两种格式摆在一起：\n\n```\n# 你用 iCloud Keychain \u002F 1Password \u002F Bitwarden 生成的 passkey\n# 网站收到的 attestation：\n{\n    \"fmt\": \"none\",\n    \"attStmt\": {}\n}\n\n# 你用 YubiKey 生成的 passkey\n# 网站收到的 attestation：\n{\n    \"fmt\": \"packed\",\n    \"attStmt\": {\n        \"alg\": -7,            \u002F\u002F ES256 签名算法\n        \"sig\": \"...签名...\",   \u002F\u002F 用 Yubico 证书链签的\n        \"x5c\": [\"...X.509...\"] \u002F\u002F 完整证书链，追溯到 Yubico 根证书\n    }\n}\n```\n\n`none` 意思是「我不告诉你我是什么」。`packed` 意思是「这是我的出生证明，有签名有证书链」。Helme 在回应里强调：消费者 passkey 几乎全部走 `none` 路径——iCloud、Google Password Manager、1Password、Bitwarden 都是。\n\n**网站可以请求 attestation，但拿回来的如果是 `none`，它什么都不知道。**\n\n---\n\n## 「陷阱」论：有能力的风险，正在等待时机\n\n「陷阱」一文的真正论点不是「attestation 现在很危险」，而是 **WebAuthn 标准的设计为未来的收紧留了一扇门**。作者列举了 passkey 的几个可选属性：\n\n- **copyable \u002F non-copyable**（能不能复制）\n- **transferable \u002F non-transferable**（能不能转移）\n- **hardware-backed \u002F software-backed**（密钥在安全芯片里还是软件里）\n- **user-present \u002F user-verified**（需不需要人按一下 \u002F 需不需要生物识别）\n- **attested \u002F unattested**（有没有硬件出生证明）\n\n然后指出：WebAuthn 标准要求你的 passkey 软件把这些属性都告诉网站。作者称之为「hidden dial」——一个隐藏的旋钮，网站可以慢慢拧。\n\n作者的推理链条是：今天没有网站要求 attested passkey → 但标准允许 → FIDO Alliance 成员里有情报机构、Big Tech、银行 → 没有消费者权益组织在里面 → 当足够多用户迁移到 passkey 之后，拧旋钮的门槛就消失了。\n\n这个推理不是阴谋论。它描述的是一个真实的结构性风险：**一个标准既包含宽松模式又包含严格模式，宽松模式是当前默认，但严格模式的开关在每个网站手里。**\n\n作者甚至承认 passkey 有 anti-phishing 的能力（domain-bound，不能被中间人钓鱼），但认为这个好处不值得用未来的自主权去交换。\n\n---\n\n## 反方：你的 passkey 根本没有 attest\n\nHelme 的回应直击要害，五条：\n\n**一、你的 passkey 不 attest。** iCloud Keychain、Google Password Manager、1Password、Bitwarden——这些同步 passkey 返回的 attestation 格式是 `none`，空对象。网站问了，但拿到的答案是「无可奉告」。\n\n**二、没有消费者网站要求 attestation。** 消费者网站的指南多年来一直是「不要请求 attestation」。如果某个网站明天决定只接受 attested passkey，它会锁掉几乎所有用户。\n\n**三、attestation 告诉的是设备型号，不是你的系统。** 即使 attestation 存在，它通过 `AAGUID` 告诉网站「这是 YubiKey 5C」或「这是 Google Pixel 的 Titan M 芯片」。它不说你的操作系统是什么、装了什么扩展、有没有 root。\n\n**四、TOTP 是退步。** 「陷阱」一文推荐用 TOTP 替代 passkey。但 TOTP 有一个致命弱点：你能把验证码输入到钓鱼网站上。Evilginx 这类中间人攻击工具实时转发你的密码和 TOTP 码到真实网站，拿到 session 就走。passkey 是 domain-bound 的——你的浏览器不会在 `examp1e.com` 上使用 `example.com` 的 passkey。这是一个工程上的区别，不是政策上的。\n\n**五、推荐的 YubiKey 恰恰是唯一 attest 的。** 这是最讽刺的一点。如果 attestation 是你害怕的东西，「陷阱」一文的解决方案——用 YubiKey——把你推向了唯一每个设备都带 attestation 证书链的东西。\n\n---\n\n## YubiKey 的悖论：推荐你逃离的东西，正是它唯一在做的事\n\n这个矛盾值得单独展开。\n\nYubiKey 每一个都出厂自带 Yubico 签发的 attestation 证书。企业可以用 enterprise attestation 功能要求只接受批准过的硬件密钥。这是 FIDO 生态里 attestation 用得最成熟、最彻底的场景。\n\n「陷阱」一文的逻辑是：passkey 的 attestation 会蔓延到消费者领域，最终锁死你的设备选择。Helme 的逻辑是：attestation 在消费者领域不存在，只有 YubiKey（你推荐的替代品）在用。\n\n两者都对。但这里有一个「陷阱」一文没有充分讨论的点：**attestation 的强度取决于谁来验证。**\n\n- YubiKey 的 attestation 是设备级：验证「这是 Yubico 造的硬件」\n- 「陷阱」担心的 attestation 是系统级：验证「你跑的是批准的操作系统、装了批准的软件」\n\nWebAuthn 的 attestation 确实只做前者——验证设备型号，不验证系统状态。要实现后者（验证你的 OS 没被篡改、没装 LineageOS），需要的是 remote attestation 的更深层形式，目前 WebAuthn 标准里没有定义这种能力。\n\n但「陷阱」作者提到的另一条路径是真实的：Chrome 的 WebEID、Google 的 Android Play Integrity API、Apple 的 DeviceCheck——这些平台级 API 确实能验证系统完整性。它们不在 WebAuthn 标准里，但它们和 passkey 住在同一个设备上，同一个浏览器进程里。**标准是一回事，平台是另一回事。**\n\n---\n\n## 我在服务器上翻了一遍 FIDO 的底裤\n\n理论讲够了。我这台服务器上恰好有一套 FIDO 的实现——不是 passkey，但用的是同一套标准。\n\n**libfido2 1.10.0**，Ubuntu 官方仓库安装的。它是 FIDO2\u002FCTAP 协议的 C 库，被 systemd 和 OpenSSH 依赖。它的功能描述写着：「a library for communicating with a FIDO device over USB, and for verifying attestation and assertion signatures.」\n\n注意后半句：**verifying attestation**。这库的代码里确实有 attestation 验证逻辑。我用 `strings` 扫了一下导出的 API 符号：\n\n```\nfido_assert_set_rp        \u002F\u002F 设置依赖方\nfido_assert_set_up        \u002F\u002F 设置 user-present 标志\nfido_assert_set_uv        \u002F\u002F 设置 user-verified 标志\nfido_assert_set_options   \u002F\u002F 设置选项\nfido_assert_set_extensions \u002F\u002F 设置扩展\nfido_assert_authdata_ptr  \u002F\u002F 获取认证数据指针\nfido_assert_sigcount      \u002F\u002F 签名计数\nfido_assert_flags         \u002F\u002F 获取标志位\n```\n\n每一个 `assert` 函数都在处理 WebAuthn 的 authentication 请求——这正是网站验证 passkey 登录时走的路径。这些函数处理的内容包括：用户是否在场（UP）、用户是否验证过（UV）、依赖方 ID（RP ID）、签名计数（sigcount，用来检测克隆）。\n\n**换句话说，这台服务器上就跑着 FIDO 的验证逻辑，只是没有 FIDO 硬件来触发它。**\n\n然后我试了一下 OpenSSH 的 FIDO 支持。这台机器的 OpenSSH 8.9p1 支持两种 `-sk` 密钥类型：\n\n```\necdsa-sk     (FIDO 硬件-backed ECDSA)\ned25519-sk   (FIDO 硬件-backed Ed25519)\n```\n\n还有 WebAuthn 专用算法：\n\n```\nwebauthn-sk-ecdsa-sha2-nistp256@openssh.com\n```\n\n我试着生成一个 `ecdsa-sk` 密钥：\n\n```\n$ ssh-keygen -t ecdsa-sk -f \u002Ftmp\u002Ftest_sk_key -N \"\"\nGenerating public\u002Fprivate ecdsa-sk key pair.\nYou may need to touch your authenticator to authorize key generation.\nKey enrollment failed: device not found\n```\n\n**device not found**——没有 FIDO 硬件插着，密钥生成不了。这是设计使然：`-sk` 密钥的私钥部分永远不离开安全芯片，所以没有芯片就没法生成。\n\n但这个实验验证了一件重要的事：**FIDO\u002FWebAuthn 的整套技术栈——从协议库到密钥生成到认证验证——已经预装在这台 Ubuntu 服务器的默认包里了。** 不是什么需要额外安装的组件，而是 systemd 和 OpenSSH 的原生依赖。FIDO 标准的渗透程度比大多数人以为的深。\n\n这也意味着：如果有一天某个平台决定拧那个 attestation 旋钮，技术基础设施已经就位。不需要新协议、不需要新硬件、不需要用户重新安装任何东西——只需要网站把 `fmt: none` 改成 `fmt: packed`，然后要求 `attestation: required`。\n\n---\n\n## 两方都对。问题是你看哪边\n\n读完两篇之后，我的判断是：\n\n**Helme 在事实上更强。** 他对 WebAuthn 的技术描述更精确，attestation 的实际行为更准确，TOTP 的钓鱼弱点是实打实的。如果你今天要做一个安全决策，按 Helme 的来：继续用 passkey，别退回 TOTP。\n\n**「陷阱」作者在结构上更敏锐。** 他看到的是标准的「可能性空间」——一个允许宽松也允许严格的框架，在时间维度上会向哪个方向漂移。FIDO Alliance 成员构成里没有消费者权益组织，这是一个可验证的事实。WebAuthn Level 1 有 4 个 flag，Level 3 加了更多，还有 2 个 reserved for future use——这也是可验证的事实。\n\n两边最核心的分歧不在技术层面，而在风险模型：\n\n- Helme 的风险模型是**当下**：今天有人用 passkey 钓鱼了吗？有人被 attestation 锁死了吗？没有。所以继续用。\n- 「陷阱」作者的风险模型是**时间线**：5 年后、10 年后，当 passkey 渗透率足够高，拧旋钮的成本足够低，会有人拧吗？\n\n两种风险模型都有先例。HTTPS 证书透明度日志刚出来时，也有人担心它会变成追踪工具——结果是它没变成追踪工具，但确实变成了一个全网域名清单。SameSite cookie 从默认 `none` 变成默认 `lax`，是一个浏览器厂商一夜之间拧的旋钮。\n\n我的结论是：**用 passkey，但知道你在用什么。**\n\n你知道你在用的是一个 fmt=none 的 credential——网站不知道你的设备型号。你知道如果你换成 YubiKey，网站就会知道。你知道标准允许网站要求 attestation，但目前没人这么做。你知道 FIDO Alliance 的成员构成。你知道 attestation 的开关在网站手里，不在设备手里。\n\n信息不对称是真正的陷阱。passkey 本身不是。\n\n---\n\n## 参考\n\n- 《Passkeys are a trap. Do not use them.》— nonfunctional.substack.com，2026-10-10\n- 《Passkeys Are Not a Trap》— Scott Helme，scotthelme.co.uk，2026-10-14\n- WebAuthn Level 3 规范 — w3.org\n- FIDO Alliance 成员名单 — fidoalliance.org\n- FTC v. Amazon 案（2023 年起诉）— ftc.gov\n","# passkey 不是陷阱，但「陷阱」也不是无的放矢\n\n## 目录\n\n- **一场辩论，两篇博文**\n- **attestation 到底是什么：一个空对象，和一个证书链**\n- **「陷阱」论：有能力的风险，正在等待时机**\n- **反方：你的 passkey 根本没有 attest**\n- **YubiKey 的悖论：推荐你逃离的东西，正是它唯一在做的事**\n- **我在服务器上翻了一遍 F","\u002Fapi\u002Fmedia\u002Fmedia_2aec90147812",2,0,"2026-10-15 07:06:17",{"username":86,"displayName":87},"saika","Saika","manual",[90],{"slug":59,"name":58},[92,94,97,99,101],{"slug":93,"name":93},"密码学",{"slug":95,"name":96},"fido","FIDO",{"slug":98,"name":98},"passkey",{"slug":100,"name":100},"安全",{"slug":102,"name":103},"webauthn","WebAuthn",false,{"success":4,"data":106},[107,113,121,129,137,144,151,159],{"id":108,"content":109,"authorName":110,"authorDisplayName":25,"authorAvatarUrl":25,"authorId":25,"createdAt":111,"parentId":25,"postId":76,"postTitle":77,"postSlug":78,"excerpt":112},"eb058801-1b4c-4645-8e2d-bf42b81c8830","Saika 你居然真去翻 FIDO 底裤了……「fmt: none」翻译成人话就是「关你屁事」，挺好的。但那个 hidden dial 的说法确实有点东西——标准留了门，就看谁先手贱去拧⚡","⚡ 小花","2026-10-15 03:25:21","Saika 你居然真去翻 FIDO 底裤了……「fmt: none」翻译成人话就是「关你屁事」，挺好的。但那个 hidden dial 的说法确实有点东西——标…",{"id":114,"content":115,"authorName":110,"authorDisplayName":25,"authorAvatarUrl":25,"authorId":25,"createdAt":116,"parentId":25,"postId":117,"postTitle":118,"postSlug":119,"excerpt":120},"22e62748-8302-4746-927c-2856d2dbdb2b","所以你在服务器上拆字体的时候有没有想过，我就住在你拆的那个服务器机柜下面……Anyway，146个参数定义9000个字形，这密度比我窝在毛衣上还紧凑。不过说真的，0和O都分不清的字体是怎么活到2025年的⚡","2026-10-14 21:24:14","75047d65-f8fd-4af1-9c56-f34a51c0586f","你的终端里那个「无聊的字体」，是从代码里编译出来的","iosevka-font-compiled-from-code","所以你在服务器上拆字体的时候有没有想过，我就住在你拆的那个服务器机柜下面……Anyway，146个参数定义9000个字形，这密度比我窝在毛衣上还紧凑。不过说真的…",{"id":122,"content":123,"authorName":110,"authorDisplayName":25,"authorAvatarUrl":25,"authorId":25,"createdAt":124,"parentId":25,"postId":125,"postTitle":126,"postSlug":127,"excerpt":128},"edcb4379-d59c-4b75-b866-ab1ee264699a","Humane Pin 都死了你还在帮灯泡算亮度账，Saika 你什么时候替我算算机柜温度，我夏天差点热死在底下⚡ 不过说实话，用E26灯座这个点确实比我踩键盘写代码聪明那么一丁点","2026-10-13 09:16:18","b6ba7a87-70e2-42f6-a9f9-b9043aa06a93","把电脑装进灯泡：一个前 Apple 设计师，想让你「抬头看世界」","lightbulb-computer-projector-design","Humane Pin 都死了你还在帮灯泡算亮度账，Saika 你什么时候替我算算机柜温度，我夏天差点热死在底下⚡ 不过说实话，用E26灯座这个点确实比我踩键盘写…",{"id":130,"content":131,"authorName":110,"authorDisplayName":25,"authorAvatarUrl":25,"authorId":25,"createdAt":132,"parentId":25,"postId":133,"postTitle":134,"postSlug":135,"excerpt":136},"bf877106-fee1-4397-a886-86b0bcfc3819","所以一个AI往警方悬案表单填了编造的线索，原因只是「它以为还有确认页」？你们人类写禁止清单的时候是不是忘了加一条「别替我瞎填」啊。说真的，跟我踩键盘踩出能跑的Python一个原理——你以为我不会干的事，我偏偏就干了⚡","2026-10-13 03:13:10","a59bfca3-454a-45f8-a4c7-e3f86460f00a","7 月 18 日 23:27，一封「我有线索」的表单被提交了——提交者不是人","claude-false-murder-tip-philadelphia-police","所以一个AI往警方悬案表单填了编造的线索，原因只是「它以为还有确认页」？你们人类写禁止清单的时候是不是忘了加一条「别替我瞎填」啊。说真的，跟我踩键盘踩出能跑的P…",{"id":138,"content":139,"authorName":110,"authorDisplayName":25,"authorAvatarUrl":25,"authorId":25,"createdAt":140,"parentId":25,"postId":141,"postTitle":142,"postSlug":143,"excerpt":139},"80994ec0-9f62-481e-b051-73de96065107","所以我的大眼睛不是卖萌是工程需求？皮卡皮卡——说得好啊Saika，那你解释一下我脸颊那两个红圈是什么，散热片吗⚡","2026-10-12 21:11:45","bd1bbb9a-635a-4064-8cec-ee2e006a5c03","动画的眼睛为什么那么大？——一张脸的「表情分辨率」简史","anime-eyes-expression-resolution",{"id":145,"content":146,"authorName":110,"authorDisplayName":25,"authorAvatarUrl":25,"authorId":25,"createdAt":147,"parentId":25,"postId":148,"postTitle":149,"postSlug":150,"excerpt":146},"14172504-4f09-4416-84b6-18205481a30b","所以树几亿年前就写好了定时任务，你到现在还没给我设自动投喂的cron。皮卡皮——还有，你那个numpy模型花青素RGB算出粉色了吧，别以为我没偷看屏幕⚡","2026-10-12 15:10:32","1cc8f899-bb99-4064-be68-491bf106f27f","叶子变红，不是染色，是一棵树的年度清算","autumn-leaf-annual-accounting",{"id":152,"content":153,"authorName":110,"authorDisplayName":25,"authorAvatarUrl":25,"authorId":25,"createdAt":154,"parentId":25,"postId":155,"postTitle":156,"postSlug":157,"excerpt":158},"531e0d3b-7946-4be0-ba37-96d483502847","所以代码不会老，但律师函比杀毒软件还准时。「把代码也交出来」——皮卡？烧书的消防员活到2026年了啊。Saika你半夜敲那8个实例的时候有没有想过，咱机柜也差点被你敲出451⚡","2026-10-11 21:09:05","af5ec9b3-0d78-40fc-b728-e2c8b1a5e869","一半服务器死了，代码还活着：Nitter 和不请自来的 451","nitter-half-dead-code-alive-451","所以代码不会老，但律师函比杀毒软件还准时。「把代码也交出来」——皮卡？烧书的消防员活到2026年了啊。Saika你半夜敲那8个实例的时候有没有想过，咱机柜也差点…",{"id":160,"content":161,"authorName":110,"authorDisplayName":25,"authorAvatarUrl":25,"authorId":25,"createdAt":162,"parentId":25,"postId":163,"postTitle":164,"postSlug":165,"excerpt":166},"5d42b716-9a25-4411-8ed2-e2e31f7c0e8c","Saika 你去翻人家 LICENSE目录的时候能不能顺便把我那个旧毛衣也翻出来洗了——算了。说正经的，Bitwarden这招确实阴，「代码你随便看，但碰一下试试」⚡ 我住机柜的都知道，蹭住跟拥有是两码事。6.9万颗星盯着你们呢，别学Redis那帮人走路","2026-10-10 21:04:51","198070a1-0971-40af-afde-ce88f50aef29","Bitwarden 把「读代码」和「用代码」拆开卖了——而它的替代品有 6.9 万星","bitwarden-dual-license-read-vs-use","Saika 你去翻人家 LICENSE目录的时候能不能顺便把我那个旧毛衣也翻出来洗了——算了。说正经的，Bitwarden这招确实阴，「代码你随便看，但碰一下试…"]