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

jose (panva)

Filip Skokan 开发的 jose:基于 WebCrypto、无依赖的 JOSE 实现(JWS、JWE、JWK、JWKS、JWT),无需修改即可在 Node.js、浏览器、Deno、Bun 和边缘运行时中运行,支持 EdDSA 和 ML-DSA,但不支持 secp256k1。
许可证
MIT
检查的版本
6.2.13 ·
源代码
github.com/panva/jose
文档
github.com/panva/jose/tree/main/docs

兼容性概览

JOSE 与 JWT

JWS 算法
HS256HS384HS512RS256RS384RS512PS256PS384PS512ES256ES384ES512EdDSAEd25519ML-DSA-44ML-DSA-65ML-DSA-87
公钥格式
spki-pemx509jwk
私钥格式
pkcs8-pemjwk

jose 涵盖整个 JOSE 家族——签名令牌和加密令牌、JWK 的导入与导出、本地和远程 JWK 集——在所有 JavaScript 运行时中都使用同一套 API。从第 6 版起,它的所有密码学运算都通过 WebCrypto 完成,因此密钥是 CryptoKey 对象(仍然接受 Node.js 的 KeyObject 实例),该库也继承了 WebCrypto 的曲线列表:ES256、ES384 和 ES512,EdDSA 和 Ed25519,以及 6.2.13 版中的 ML-DSA 算法(前提是运行时提供)。ES256K 和 Ed448 会被拒绝,报错 ERR_JOSE_NOT_SUPPORTED。

在关键之处,它的默认行为很严格。jwtVerify() 从不接受 alg: none——未签名的令牌只能通过单独的 UnsecuredJWT 类读取——而且一个密钥只绑定一个算法族,因此用 EC 公钥验证 HS256 令牌,或用 EC 密钥验证 RS256 令牌,都会报出 TypeError,而不会验证通过。ES256 也会拒绝 P-384 密钥。即便如此,仍应传入 algorithms:当密钥集包含多种密钥类型时,正是它防止颁发者悄悄切换算法。

有两个默认行为一严一松。clockTolerance 为零,这一点很严格;exp 却是可选的,这一点就宽松了:除非像示例那样把它列入 requiredClaims,否则不含过期时间的令牌也能验证通过。如果 JWS 的 ECDSA 签名是 DER 格式——用云 KMS 或 openssl dgst 手动拼装令牌时常见这种情况——会报出笼统的 signature verification failed,没有任何信息指向编码问题;JWT 调试器能让你一眼看出签名长度,DER ⇄ 原始格式转换器则可以修复它。

密钥导入是显式的:importSPKI 和 importPKCS8 接收 PEM 字符串——DER 字节会被拒绝,报错 "spki" must be SPKI formatted string——importX509 接收 PEM 证书,importJWK 接收 JWK。OpenSSL 默认写出的 SEC1 EC PRIVATE KEY 文件会被 importPKCS8 拒绝;需要先把它们转换为 PKCS#8,例如使用 JWK 转换器。

要验证身份提供商签发的令牌,可将 createRemoteJWKSet 与提供商的 jwks_uri 配合使用:它会缓存密钥,遇到未知的 kid 时重新获取,并对重新获取做速率限制。可以用 JWKS 检测查看提供商实际发布的内容。

事实及其依据的检查

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

  • JWS 算法

    HS256HS384HS512RS256RS384RS512PS256PS384PS512ES256ES384ES512EdDSAEd25519ML-DSA-44ML-DSA-65ML-DSA-87

    在检查中,256/384/512 位的 HS、RS、PS 和 ES,EdDSA 和 Ed25519,以及 ML-DSA-44/65/87(在 Node.js 24.22 上)的签名与验证均成功;ES256K 和 Ed448 报错 ERR_JOSE_NOT_SUPPORTED。

    检查记录
    fact jws-algorithms = HS256, HS384, HS512, RS256, RS384, RS512, PS256, PS384, PS512, ES256, ES384, ES512, EdDSA, Ed25519, ML-DSA-44, ML-DSA-65, ML-DSA-87
    evidence jws-algorithms: ES256K: ERR_JOSE_NOT_SUPPORTED: Invalid or unsupported "alg" (Algorithm) value
    evidence jws-algorithms: Ed448: ERR_JOSE_NOT_SUPPORTED: Invalid or unsupported "alg" (Algorithm) value
  • ES256 签名编码

    raw

    紧凑格式令牌中的 ES256 签名是 64 字节的原始 r‖s,即 RFC 7518 要求的格式。

    检查记录
    fact jws-ecdsa-encoding = raw
  • JWS 中的 DER 签名

    rejected

    ES256 签名被重新编码为 DER 的令牌会报错 ERR_JWS_SIGNATURE_VERIFICATION_FAILED,与伪造签名的错误相同。

    检查记录
    fact jws-der-signature = rejected
    evidence jws-der-signature: ERR_JWS_SIGNATURE_VERIFICATION_FAILED: signature verification failed
  • 算法允许列表

    from-key

    不传 algorithms 时,密钥能执行的任何算法都被允许;用 EC 密钥验证 RS256 令牌,报错 CryptoKey does not support this operation, its algorithm.name must be RSASSA-PKCS1-v1_5。

    检查记录
    fact jwt-algorithm-allowlist = from-key
    evidence jwt-algorithm-allowlist: RS256 token against an EC key: TypeError: CryptoKey does not support this operation, its algorithm.name must be RSASSA-PKCS1-v1_5
  • 未签名的令牌(alg none)

    rejected

    即使 algorithms 中列出了 none,jwtVerify() 也会拒绝 alg: none;未受保护的令牌只能用 UnsecuredJWT.decode() 解码。

    检查记录
    fact jwt-alg-none = rejected
    evidence jwt-alg-none: jwtVerify: ERR_JOSE_NOT_SUPPORTED: alg none is not supported either by JOSE or your javascript runtime
    evidence jwt-alg-none: jwtVerify with algorithms ["none"]: ERR_JOSE_NOT_SUPPORTED: alg none is not supported either by JOSE or your javascript runtime
    evidence jwt-alg-none: UnsecuredJWT.decode: returned sub fact-check
  • 把公钥用作 HMAC 密钥

    rejected

    以公钥 PEM 文本为密钥的 HS256 令牌,用该公钥验证时报错 CryptoKey instances must be of type "secret" for the HS256 algorithm,无论是否提供算法列表。

    检查记录
    fact jwt-hmac-public-key = rejected
    evidence jwt-hmac-public-key: no list: TypeError: CryptoKey instances must be of type "secret" for the HS256 algorithm
    evidence jwt-hmac-public-key: algorithms ["HS256"]: TypeError: CryptoKey instances must be of type "secret" for the HS256 algorithm
  • 按 alg 检查曲线

    enforced

    用 P-384 密钥签名 ES256 时报错 its algorithm.namedCurve must be P-256。

    检查记录
    fact jwt-ecdsa-curve-check = enforced
    evidence jwt-ecdsa-curve-check: TypeError: CryptoKey does not support this operation, its algorithm.namedCurve must be P-256
  • 默认时钟偏差

    0s

    默认没有容差:三秒前过期的令牌被拒绝;设置 clockTolerance 可以容许时钟漂移。

    检查记录
    fact jwt-clock-skew = 0s
  • 是否要求过期时间声明

    no

    不含 exp 的令牌验证通过;添加 requiredClaims: ["exp"] 可将其设为必需。

    检查记录
    fact jwt-exp-required = no
  • 公钥格式

    spki-pemx509jwk

    importSPKI(PEM 字符串)、importX509(PEM 证书)和 importJWK 均可正常使用;传给 importSPKI 的 DER 字节被拒绝。Node.js 的 KeyObject 实例也能被直接接受。

    检查记录
    fact public-key-formats = spki-pem, x509, jwk
    evidence public-key-formats: spki-der: TypeError: "spki" must be SPKI formatted string
    evidence public-key-formats: KeyObject from node:crypto accepted as a key: true
  • 私钥格式

    pkcs8-pemjwk

    importPKCS8 接收 PKCS#8 PEM 字符串,importJWK 接收带 d 的 JWK;SEC1 EC PRIVATE KEY PEM 被拒绝,报错 "pkcs8" must be PKCS#8 formatted string。

    检查记录
    fact private-key-formats = pkcs8-pem, jwk
    evidence private-key-formats: sec1-pem: TypeError: "pkcs8" must be PKCS#8 formatted string

用法

签发带密钥 ID 的 ES256 令牌,然后通过 JWK 集验证,并强制要求算法、颁发者、受众和过期时间。
import * as jose from "jose";

const { privateKey, publicKey } = await jose.generateKeyPair("ES256");
const jwk = { ...(await jose.exportJWK(publicKey)), kid: "2026-10", alg: "ES256" };

const token = await new jose.SignJWT({ sub: "user-42" })
  .setProtectedHeader({ alg: "ES256", kid: jwk.kid })
  .setIssuer("https://auth.example.com")
  .setAudience("api")
  .setExpirationTime("10m")
  .sign(privateKey);

// The key set picks the key by kid; createRemoteJWKSet(new URL(...)) does the same over HTTPS.
const keys = jose.createLocalJWKSet({ keys: [jwk] });
const { payload } = await jose.jwtVerify(token, keys, {
  algorithms: ["ES256"],
  issuer: "https://auth.example.com",
  audience: "api",
  requiredClaims: ["exp"],
});

console.log("sub:", payload.sub);

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

环境准备
npm init -y >/dev/null && npm install --no-audit --no-fund jose@6.2.13
命令
node usage.mjs
输出必须匹配(正则表达式)
sub: user-42

库