跳到主要内容
ecdsa.com
Node.js · WebCrypto

SimpleWebAuthn

Matthew Miller 开发的 SimpleWebAuthn,TypeScript 中使用最广泛的通行密钥库:提供选项生成器和响应验证器,默认要求用户验证,断言签名只接受 DER,并拒绝计数器回退。
许可证
MIT
检查的版本
14.0.3 ·
源代码
github.com/MasterKale/SimpleWebAuthn
文档
simplewebauthn.dev/docs/

兼容性概览

WebAuthn

SimpleWebAuthn 把 WebAuthn 拆成两个包:@simplewebauthn/browser 封装 navigator.credentials,@simplewebauthn/server 生成依赖方发送的选项,并验证返回的内容——证明格式、COSE 密钥、计数器和签名。本次检查用一个软件认证器驱动服务端包,该认证器生成标准的注册和断言 JSON,与 Python、Go 和 Java 页面所用的字节相同。

最常见的集成缺陷是选项所要求的与验证所要求的不一致。generateRegistrationOptions() 默认使用 userVerification: "preferred",但两个验证器都默认 requireUserVerification: true:认证器只证明了用户在场时——例如没有设置 PIN 的安全密钥——其断言被拒绝,报错 User verification required, but user could not be verified。应一次性做出决定,并像示例那样让两端的设置保持一致。

按照 WebAuthn 规范的要求,ES256 的断言签名必须是 ASN.1 DER。64 字节的原始 r‖s 签名照样会被送进 ASN.1 解析器,报出的消息取决于签名字节——在 60 个新建凭证上最常见的是 Too big integer、End of input reached before message was fully decoded 或 Data does not match to ECDSASigValue ASN1 schema.——这些消息都没有提到编码。认证器和通行密钥提供方生成的都是 DER,因此出现原始签名通常意味着客户端是手写的,或者测试工具直接调用了 WebCrypto 的 sign();可以用 DER ⇄ 原始格式转换器转换这些签名。high-S 的 DER 签名验证通过。

签名计数器低于已存储的值会被视为错误,而不是警告:验证时抛出了 Response counter value 3 was lower than expected 5,已存储值为 5 时,计数器为 0 也同样失败。只有一直报告 0 的凭证——同步通行密钥就是如此——才能以 0 通过。应保存收到的计数器值,并自行决定回退是锁定该凭证,还是只发出告警。14 版在默认的 pubKeyCredParams 中把 ML-DSA-44(COSE -48)列在第一位,排在 Ed25519(-8)、ES256(-7)和 RS256(-257)之前,Ed25519 凭证也完成了端到端验证。

用 WebAuthn 查看器解码浏览器实际发送的数据,在通行密钥沙盒中无需真实认证器即可试用各个流程,并用 AAGUID 查询按 AAGUID 识别认证器型号。

事实及其依据的检查

以下内容均非抄自文档。检查脚本 content/libraries/verify/simplewebauthn/run.sh 于2026年10月20日在容器镜像 node:24.22.0-bookworm-slim 中运行了 SimpleWebAuthn 14.0.3(运行时为 Node.js 24.22.0);本页内容必须与它的输出一致,每条事实都列出了对应的输出行。

  • 默认 COSE 算法

    -48-8-7-257

    generateRegistrationOptions() 依次列出 COSE 算法 -48(ML-DSA-44)、-8(Ed25519)、-7(ES256)和 -257(RS256)。

    检查记录
    fact webauthn-default-algorithms = -48, -8, -7, -257
  • 默认证明方式

    none

    证明传递方式默认为 none。

    检查记录
    fact webauthn-attestation = none
  • 默认用户验证

    preferred

    authenticatorSelection.userVerification 默认为 preferred。

    检查记录
    fact webauthn-user-verification = preferred
  • 默认驻留密钥

    preferred

    authenticatorSelection.residentKey 默认为 preferred。

    检查记录
    fact webauthn-resident-key = preferred
  • 默认强制用户验证

    yes

    除非传入 requireUserVerification: false,两个验证器都要求设置 UV 标志:只设置了 UP 的注册和断言被拒绝,分别报错 User verification was required, but user could not be verified 和 User verification required, but user could not be verified。

    检查记录
    fact webauthn-uv-enforced-by-default = yes
    evidence webauthn-uv-enforced-by-default: registration with the UV flag clear: throws User verification was required, but user could not be verified
  • 断言签名编码

    der

    带 ASN.1 DER 签名的正确 ES256 断言验证通过。

    检查记录
    fact webauthn-assertion-encoding = der
  • 原始 r‖s 断言签名

    rejected

    同一断言改用 64 字节的原始 r‖s 签名后,抛出了 ASN.1 解析错误,其措辞取决于签名字节。

    检查记录
    fact webauthn-raw-signature = rejected
  • high-S 断言签名

    accepted

    把 s 替换为 n − s 的 DER 签名验证通过。

    检查记录
    fact webauthn-high-s = accepted
  • 签名计数器回退

    rejected

    计数器低于已存储值时抛出 Response counter value 3 was lower than expected 5;已存储值为 5 时,计数器为 0 也被拒绝,而已存储值为 0 时,计数器为 0 验证通过。

    检查记录
    fact webauthn-counter-regression = rejected
    evidence webauthn-counter-regression: counter 0 after a stored 5: throws Response counter value 0 was lower than expected 5; counter 0 after a stored 0: verified
  • Ed25519 凭证

    supported

    Ed25519(COSE -8)凭证的注册和身份验证均成功。

    检查记录
    fact webauthn-ed25519 = supported
    evidence webauthn-ed25519: verified

用法

生成注册选项和身份验证选项,使其要求的用户验证策略与验证器默认强制执行的策略相同。
import { generateAuthenticationOptions, generateRegistrationOptions } from "@simplewebauthn/server";

const rpID = "example.com";

// Registration: the options ask for UV only as "preferred"...
const registration = await generateRegistrationOptions({
  rpName: "Example",
  rpID,
  userName: "user@example.com",
  authenticatorSelection: { residentKey: "required", userVerification: "required" },
});

// ...while verifyRegistrationResponse() and verifyAuthenticationResponse()
// require the UV flag by default. Ask for "required" up front (as here), or pass
// requireUserVerification: false when a security key without a PIN is acceptable.
const authentication = await generateAuthenticationOptions({ rpID, userVerification: "required" });

console.log("algorithms:", registration.pubKeyCredParams.map((param) => param.alg).join(", "));
console.log("uv:", registration.authenticatorSelection?.userVerification, authentication.userVerification);
console.log("challenge bytes:", Buffer.from(authentication.challenge, "base64url").length);

代码片段测试工具会在容器镜像 node:24.22.0-bookworm-slim 中原样运行每个示例,只要输出不匹配,运行就会失败。

环境准备
npm init -y >/dev/null && npm install --no-audit --no-fund @simplewebauthn/server@14.0.3
命令
node usage.mts
输出必须匹配(正则表达式)
algorithms: -48, -8, -7, -257\nuv: required required\nchallenge bytes: 32

库