Pular para o conteúdo
ecdsa.com
Node.js · WebCrypto

jose (panva)

O jose, de Filip Skokan: uma implementação JOSE sem dependências (JWS, JWE, JWK, JWKS, JWT) sobre o WebCrypto, que roda sem alterações em Node.js, navegadores, Deno, Bun e runtimes de edge, com EdDSA e ML-DSA, mas sem secp256k1.
Licença
MIT
Versão verificada
6.2.13 ·
Código-fonte
github.com/panva/jose
Documentação
github.com/panva/jose/tree/main/docs

Visão geral da compatibilidade

JOSE e JWT

Algoritmos JWS
HS256HS384HS512RS256RS384RS512PS256PS384PS512ES256ES384ES512EdDSAEd25519ML-DSA-44ML-DSA-65ML-DSA-87

O jose cobre toda a família JOSE — tokens assinados e criptografados, importação e exportação de JWK, conjuntos de chaves JWK locais e remotos — com uma única API em todos os runtimes JavaScript. Desde a versão 6, ele faz toda a criptografia por meio do WebCrypto; por isso, as chaves são objetos CryptoKey (instâncias de KeyObject do Node.js continuam aceitas), e a biblioteca herda a lista de curvas do WebCrypto: ES256, ES384 e ES512, EdDSA e Ed25519 e, na versão 6.2.13, os algoritmos ML-DSA onde o runtime os oferece. ES256K e Ed448 são recusados com ERR_JOSE_NOT_SUPPORTED.

Os padrões são estritos onde importa. jwtVerify() nunca aceita alg: none — tokens sem assinatura só podem ser lidos pela classe separada UnsecuredJWT —, e uma chave fica vinculada a uma única família de algoritmos; assim, um token HS256 apresentado com uma chave pública EC, ou um token RS256 com uma chave EC, falha com um TypeError em vez de ser verificado. O ES256 também recusa uma chave P-384. Passe algorithms mesmo assim: com um conjunto de chaves que contém vários tipos de chave, é isso que impede que um emissor troque de algoritmo silenciosamente.

Dois padrões apontam em direções opostas. clockTolerance é zero, o que é estrito, e exp é opcional, o que não é: um token sem expiração é verificado, a menos que você liste essa claim em requiredClaims, como faz o exemplo. Um JWS cuja assinatura ECDSA chegou em DER — algo típico quando um token é montado à mão com um KMS de nuvem ou com openssl dgst — falha com o genérico signature verification failed, sem nada que aponte para a codificação; o depurador de JWT mostra o tamanho da assinatura num relance, e o conversor DER ⇄ bruto corrige o problema.

A importação de chaves é explícita: importSPKI e importPKCS8 recebem strings PEM — bytes DER são recusados com "spki" must be SPKI formatted string —, importX509 recebe um certificado PEM e importJWK recebe uma JWK. Os arquivos SEC1 EC PRIVATE KEY que o OpenSSL grava por padrão são rejeitados por importPKCS8; converta-os antes para PKCS#8, por exemplo com o conversor de JWK.

Para verificar tokens de um provedor de identidade, use createRemoteJWKSet com o jwks_uri do provedor: ele mantém as chaves em cache, busca o conjunto de novo diante de um kid desconhecido e aplica um limite de taxa a essas novas buscas. Confira o que o provedor realmente publica com o verificador de JWKS.

Os fatos e as verificações por trás deles

Nada abaixo foi copiado da documentação. A verificação content/libraries/verify/jose/run.sh executou jose (panva) 6.2.13 em Node.js 24.22.0 na imagem de contêiner node:24.22.0-bookworm-slim em 20 de outubro de 2026; a página precisa corresponder ao que a verificação imprimiu, e cada fato lista essas linhas.

  • Algoritmos JWS

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

    No teste, assinatura e verificação funcionaram com HS, RS, PS e ES em 256/384/512 bits, com EdDSA e Ed25519 e com ML-DSA-44/65/87 (no Node.js 24.22); ES256K e Ed448 lançaram ERR_JOSE_NOT_SUPPORTED.

    Registrado pela verificação
    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
  • Codificação da assinatura ES256

    raw

    As assinaturas ES256 em tokens compactos são 64 bytes de r‖s brutos, a forma que a RFC 7518 exige.

    Registrado pela verificação
    fact jws-ecdsa-encoding = raw
  • Assinatura DER dentro de um JWS

    rejected

    Um token cuja assinatura ES256 foi recodificada em DER falha com ERR_JWS_SIGNATURE_VERIFICATION_FAILED, o mesmo erro de uma assinatura forjada.

    Registrado pela verificação
    fact jws-der-signature = rejected
    evidence jws-der-signature: ERR_JWS_SIGNATURE_VERIFICATION_FAILED: signature verification failed
  • Lista de algoritmos permitidos

    from-key

    Sem algorithms, qualquer algoritmo que a chave consiga executar é permitido; um token RS256 verificado com uma chave EC falhou com CryptoKey does not support this operation, its algorithm.name must be RSASSA-PKCS1-v1_5.

    Registrado pela verificação
    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
  • Tokens sem assinatura (alg none)

    rejected

    jwtVerify() recusa alg: none mesmo quando none está listado em algorithms; tokens sem assinatura só podem ser decodificados com UnsecuredJWT.decode().

    Registrado pela verificação
    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
  • Chave pública usada como segredo HMAC

    rejected

    Um token HS256 que usava como chave o texto PEM da chave pública falha contra essa chave pública com CryptoKey instances must be of type "secret" for the HS256 algorithm, com ou sem lista de algoritmos.

    Registrado pela verificação
    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
  • Curva conferida com o alg

    enforced

    Assinar ES256 com uma chave P-384 falha com its algorithm.namedCurve must be P-256.

    Registrado pela verificação
    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
  • Tolerância padrão de diferença de relógio

    0s

    Sem tolerância por padrão: um token que tinha expirado três segundos antes foi rejeitado; defina clockTolerance para compensar a diferença de relógio.

    Registrado pela verificação
    fact jwt-clock-skew = 0s
  • Claim de expiração obrigatória

    no

    Um token sem exp foi verificado; adicione requiredClaims: ["exp"] para exigi-la.

    Registrado pela verificação
    fact jwt-exp-required = no
  • Formatos de chave pública

    spki-pemx509jwk

    importSPKI (string PEM), importX509 (certificado PEM) e importJWK funcionaram; bytes DER passados a importSPKI foram recusados. Instâncias de KeyObject do Node.js também foram aceitas diretamente.

    Registrado pela verificação
    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
  • Formatos de chave privada

    pkcs8-pemjwk

    importPKCS8 recebe uma string PEM PKCS#8 e importJWK, uma JWK com d; um PEM SEC1 EC PRIVATE KEY foi recusado com "pkcs8" must be PKCS#8 formatted string.

    Registrado pela verificação
    fact private-key-formats = pkcs8-pem, jwk
    evidence private-key-formats: sec1-pem: TypeError: "pkcs8" must be PKCS#8 formatted string

Uso

Emita um token ES256 com um ID de chave e depois verifique-o por meio de um conjunto de chaves JWK, exigindo algoritmo, emissor, público-alvo e expiração.
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);

O executor de trechos de código roda cada exemplo literalmente na imagem de contêiner node:24.22.0-bookworm-slim, e a execução falha se a saída não corresponder.

Configuração
npm init -y >/dev/null && npm install --no-audit --no-fund jose@6.2.13
Comando
node usage.mjs
A saída deve corresponder a (expressão regular)
sub: user-42

Bibliotecas