Enforce per-credential user verification at assertion time #9

Closed
opened 2026-07-28 12:21:24 +09:00 by FLYEMOJ1 · 1 comment
FLYEMOJ1 commented 2026-07-28 12:21:24 +09:00 (Migrated from github.com)

问题

webauthn_credentials 上持久化了每个凭据的 user_verified / backup_eligible / backup_state,但没有任何断言路径读它们。目前的保证来自全局策略 —— 注册和 discoverable login 都写死 VerificationRequired —— 而不是来自这几列。

现在这不构成漏洞:全局要求 UV,所以每一行的 user_verified 都是 true。它在两种情况下变成问题:策略放宽到 preferred,或者引入一条不走这两个入口的登录/提权路径。migration 0009 的注释本身就是这么写的。

证据

Identified by Claude Opus 5 (claude-opus-5) while auditing the WebAuthn subsystem's persisted flags on 2026-07-28.

internal/store/migrations/0009_*.sql 存了这三列,注释写明用意:

--   user_verified   — a PIN/biometric (not mere presence) was performed at bind. With the
--                     required-UV policy this is always true for new rows, but persisting
--                     it survives a future policy that permits UV=preferred credentials.

消费侧目前只有全局策略两处,没有按凭据的判定:

  • internal/passkey/verifier.go:74 — UserVerification: protocol.VerificationRequired
  • internal/passkey/verifier.go:229 — BeginDiscoverableLogin(webauthn.WithUserVerification(protocol.VerificationRequired))

grep -rn "user_verified" --include=*.go internal/ 在断言路径上没有命中。

验收

断言完成时按凭据校验 UV:一个 user_verified = false 的凭据在需要 UV 的操作上被拒绝。

备注

这条属于 task #40(passkey 登录/提权)的范围,在那条落地之前没有单独做的必要 —— 现在没有断言路径可以挂。开这条是为了别在建 #40 的时候把这个已经存好的信息忘掉,重新去做一个全局开关。

## 问题 `webauthn_credentials` 上持久化了每个凭据的 `user_verified` / `backup_eligible` / `backup_state`,但没有任何断言路径读它们。目前的保证来自全局策略 —— 注册和 discoverable login 都写死 `VerificationRequired` —— 而不是来自这几列。 现在这不构成漏洞:全局要求 UV,所以每一行的 `user_verified` 都是 true。它在两种情况下变成问题:策略放宽到 `preferred`,或者引入一条不走这两个入口的登录/提权路径。migration 0009 的注释本身就是这么写的。 ## 证据 Identified by Claude Opus 5 (claude-opus-5) while auditing the WebAuthn subsystem's persisted flags on 2026-07-28. `internal/store/migrations/0009_*.sql` 存了这三列,注释写明用意: ``` -- user_verified — a PIN/biometric (not mere presence) was performed at bind. With the -- required-UV policy this is always true for new rows, but persisting -- it survives a future policy that permits UV=preferred credentials. ``` 消费侧目前只有全局策略两处,没有按凭据的判定: - `internal/passkey/verifier.go:74` — `UserVerification: protocol.VerificationRequired` - `internal/passkey/verifier.go:229` — `BeginDiscoverableLogin(webauthn.WithUserVerification(protocol.VerificationRequired))` `grep -rn "user_verified" --include=*.go internal/` 在断言路径上没有命中。 ## 验收 断言完成时按凭据校验 UV:一个 `user_verified = false` 的凭据在需要 UV 的操作上被拒绝。 ## 备注 这条属于 task #40(passkey 登录/提权)的范围,在那条落地之前没有单独做的必要 —— 现在没有断言路径可以挂。开这条是为了别在建 #40 的时候把这个已经存好的信息忘掉,重新去做一个全局开关。
Lemon-miaow commented 2026-09-26 08:12:36 +09:00 (Migrated from github.com)

9ab7b27 落地:applyAssertion 是三个断言入口(用户名登录、无用户名登录、提权/迁移确认)共用的唯一判定点,要求凭据绑定时 user_verified=true 且本次断言也带 UV,任一缺失按 passkey_login_invalid 统一拒绝并记 auth.passkey_uv_rejected 审计。顺带修掉一个真实缺陷:WebAuthnCredentials 没把存储的 BE/BS 标志交给 go-webauthn,而 v0.17 会拒绝 BE 与存储不一致的断言,云同步 passkey(iCloud 钥匙串、Google 密码管理器)此前能注册、登不进。测试:virtualwebauthn 的 BE=1 认证器走两种登录入口(修复前红),三入口 × 三种缺 UV 情形;变异验证全部击杀。

9ab7b27 落地:applyAssertion 是三个断言入口(用户名登录、无用户名登录、提权/迁移确认)共用的唯一判定点,要求凭据绑定时 user_verified=true 且本次断言也带 UV,任一缺失按 passkey_login_invalid 统一拒绝并记 auth.passkey_uv_rejected 审计。顺带修掉一个真实缺陷:WebAuthnCredentials 没把存储的 BE/BS 标志交给 go-webauthn,而 v0.17 会拒绝 BE 与存储不一致的断言,云同步 passkey(iCloud 钥匙串、Google 密码管理器)此前能注册、登不进。测试:virtualwebauthn 的 BE=1 认证器走两种登录入口(修复前红),三入口 × 三种缺 UV 情形;变异验证全部击杀。
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: FelisMC/Felis#9