前言
不同网站的密码需要管理,不仅容易忘记,也存在被窃取或重复使用带来的风险。如果只需要扫描指纹或使用脸部识别,就能立即完成注册与登入——这正是 FIDO(Fast IDentity Online)所推动的「无密码身分验证」愿景。
透过 FIDO 能享受以下优势:
- 伺服器没有储存密码就不怕被偷
- 攻击者无法透过网路拦截,因为私钥从头到尾都不会被传输
- 绑定网站的来源(Origin)并强制使用 HTTPS 建立安全连线。就算用户误入以假乱真的钓鱼网站,浏览器也不会放行。
- 可搭配生物辨识提升「解锁私钥」的安全性
FIDO 大概念
利用非对称式加密来验证身份,彻底抛弃帐号密码
- 挑战(challenge):每次请求都不同且只能用一次的随机字串,用于让私钥进行签章
- 签章(signature):拿私钥与挑战进行运算的动作或结果
- FIDO 注册:用户产生一组「公钥 / 私钥」,网站保存公钥,用户保存私钥
- FIDO 登入:验证用户持有私钥、用户回应是针对挑战即时产生的(防重放攻击)
- 伺服器产生挑战
- 使用者的装置用私钥对挑战进行签章
- 伺服器使用公钥来验证签章是否正确
- 生物识别:用户解锁私钥的一种方式,例如指纹。
- Passkey:一种管理私钥的机制。建构在「私钥」技术之上的一套数位钥匙圈系统,让私钥变得好懂好管且不怕弄丢。
FIDO 实践重点
FIDO 之所以可行是因为浏览器提供了 Web Authentication API 能安全与私钥互动、用户验证、Origin 绑定、装置绑定。
-
用户 →
navigator.credentials.create() -
OS / 硬体帮你产 key
-
私钥锁在装置
-
公钥给伺服器
-
用户 →
navigator.credentials.get() -
用私钥签挑战
-
回传签章
-
伺服器用公钥验证
FIDO 流程图
注册
登入
实践
注册
- 阶段一:生成 (Begin)
- 使用者登入或刚注册后,申请绑定 Passkey。
- 前端呼叫:
POST /auth/passkey/register/begin。 - 后端回传资讯:
challenge:一次性随机字串。user资讯:包含id,name,displayName。relying party(RP):网站 Domain 或rpId。pubKeyCredParams:支援的加密演算法。
- 前端执行
navigator.credentials.create():
-
产生公钥与私钥、对挑战进行 Attestation(证明金钥来源与合法性)
-
阶段二:验证 (Finish)
- 前端将结果送回后端:
POST /auth/passkey/register/finish。 - 后端进行多重验证:
- 一致性:挑战是否与先前发出的一致(防重放攻击)。
- 合法性:Attestation 是否合法(确认装置与金钥可信)。
- 来源:Origin /
rpId是否正确(防止钓鱼网站)。
- 验证成功后存档:
public key、credential id、counter(计数器) 与user关联。
- 完成注册:该使用者已成功绑定一组 Passkey。
登入
- 阶段一:生成 (Begin)
- 使用 FIDO 登入
- 前端呼叫:
POST /auth/passkey/login/begin。 - 后端回传资讯:
challenge:挑战字串。allowCredentials:该使用者已注册过的 Credential IDs(若留空则代表使用 Discoverable Credentials 模式)。
- 前端执行
navigator.credentials.get():
-
使用者认证:透过指纹、FaceID 或 PIN 码解锁私钥。
-
签署:使用私钥对挑战进行签章。
-
阶段二:验证 (Finish)
- 前端将签章结果送回后端:
POST /auth/passkey/login/finish。 - 后端验证逻辑:
- 挑战码:挑战是否一致。
- 签章:使用资料库中的公钥验证签章是否正确。
- 计数器:检查
counter是否大于前次(防止重放或复制装置)。 - 来源:Origin /
rpId是否正确。
- 验证成功后处理:
- 更新资料库中的
counter。 - 建立登入状态(签发 Session 或 JWT)。
- 完成登入:实现无密码安全登入。
可发现凭证(驻留金钥)
传统「不可发现凭证 Non-Discoverable」的验证方式伺服器必须先知道你是谁(先输入帐号),伺服器才能从资料库找出对应的 Credential ID 送给认证器。认证器解密后才能用私钥签章。
而现代认证器空间够大。注册时,它不仅产生公私钥,还会把私钥、使用者 ID、帐号名称、以及网站的网域 (RP ID) 全部存在认证器。
登入时伺服器不需要知道你是谁,只要问认证器:「你有这个网站(RP ID)的凭证吗?」认证器就会在本地「发现」这些凭证,并跳出选单问你要用哪一个帐号登入,完全不需要输入帐号。
注册
navigator.credentials.create({ publicKey: { // ... 其他设定 authenticatorSelection: { authenticatorAttachment: "platform", // 要求建立可发现凭证 residentKey: "required", userVerification: "required" } }});residentKeyrequired:必须建立可供搜寻的凭证。如果无法建立,系统会传回NotSupportedError。preferred:RP 偏好建立可供探索的凭证,但也接受不可探索的凭证。discouraged:RP 偏好建立不可侦测的凭证,但也接受可侦测的凭证。- requireResidentKey:
为了与旧版规格的 WebAuthn Level 1 保持回溯相容性,这个属性会保留。如果 residentKey 是
required,请将此值设为 true,否则请设为 false。
登入
在发起验证请求时,你不需要在 allowCredentials 中带入特定的 Credential ID。
navigator.credentials.get({ publicKey: { challenge: serverGeneratedChallenge, rpId: "yourdomain.com", // 留空或不提供 allowCredentials 阵列 // 认证器会自动根据 rpId 去本地「发现」所有可用的帐号 allowCredentials: [], userVerification: "required" }});总结
FIDO 规格文件有明确的验证步骤确保不产生安全漏洞,以及不同浏览器与平台的差异,实战上最好依靠现成套件来处理,这也是为什么会有这么多 FIDO 套件,把所有复杂情境包装成 verifyRegistrationResponse() 和 verifyAuthenticationResponse() 能够直接使用。