Assinaturas digitais garantem que uma mensagem ou ficheiro não foi alterado e que foi criado por quem afirma ser o autor. Em Node.js, o módulo nativo crypto suporta dois algoritmos modernos: ECDSA (Elliptic Curve Digital Signature Algorithm) e Ed25519. Este tutorial mostra como implementar ambos do zero, com código funcional, tabelas comparativas e 12 passos concretos para um projeto completo.
Ed25519 verifica assinaturas cerca de 10 vezes mais rápido do que RSA-2048 e gera chaves públicas de apenas 32 bytes. ECDSA com a curva P-256 domina os ecossistemas TLS e JWT. Saber qual algoritmo usar e implementá-lo corretamente evita vulnerabilidades críticas como reutilização de nonce e ataques de curva inválida, dois dos erros mais explorados em criptografia assimétrica.
O Que São Assinaturas Digitais e Por Que Importam em 2026
Uma assinatura digital é o equivalente criptográfico de uma assinatura manuscrita, mas com garantias matemáticas verificáveis. Funciona com um par de chaves: a chave privada assina os dados, e a chave pública verifica a assinatura. Qualquer pessoa com a chave pública pode confirmar que o detentor da chave privada assinou aquele conteúdo específico, sem nunca ter acesso à chave privada em si.
Em 2026, assinaturas digitais estão presentes em praticamente toda infraestrutura moderna. O protocolo TLS 1.3 usa ECDSA ou Ed25519 para autenticar certificados durante o handshake. JWTs assinados com o algoritmo ES256 (ECDSA + SHA-256) protegem APIs REST em todo o mundo. O sistema de assinatura de código do npm verifica a integridade dos pacotes antes da instalação. SSH usa Ed25519 por padrão nas configurações mais recentes por ser mais rápido e seguro do que RSA.
A diferença entre ECDSA e Ed25519 é mais do que técnica. ECDSA requer um número aleatório único, o nonce k, a cada assinatura. Se esse número se repetir, mesmo uma única vez, a chave privada pode ser matematicamente recuperada. Ed25519 é determinístico: dado o mesmo par chave privada e mensagem, produz sempre a mesma assinatura, eliminando por completo a dependência de um gerador de números aleatórios de boa qualidade durante a assinatura.
O NIST publicou o FIPS 186-5 em 2023, atualizando os padrões de assinaturas digitais para incluir curvas EdDSA (que inclui Ed25519) ao lado das curvas ECDSA tradicionais. A RFC 8032 especifica Ed25519 em detalhe. A RFC 7518 define como usar ECDSA em JWTs (algoritmos ES256, ES384 e ES512). Qualquer implementação séria de assinaturas digitais em Node.js parte destas três referências normativas.
O módulo crypto do Node.js, baseado em OpenSSL, suporta todos estes algoritmos nativamente desde o Node.js 12. Não precisa de instalar nenhuma dependência externa para ECDSA ou Ed25519. A API estabilizou nas versões recentes, com o Node.js 26.0.0 a adicionar suporte ao parâmetro de contexto para Ed25519, relevante para protocolos que vinculam assinaturas a um domínio específico. A documentação completa está disponível em nodejs.org/api/crypto.html.
Pré-requisitos: Versões e Conhecimentos Necessários
Antes de começar, certifique-se de que o seu ambiente cumpre estes requisitos:
| Requisito | Versão Mínima | Recomendada | Motivo |
|---|---|---|---|
| Node.js | 18.x LTS | 22.x LTS | Ed25519 estável e API crypto completa |
| npm | 9.x | 10.x | Suporte a workspaces e lockfile v3 |
| OpenSSL | 3.0 | 3.x mais recente | Incluído com Node.js 18+ |
| Sistema Operativo | Linux / macOS / Windows 10 | Linux ou macOS | Melhor compatibilidade de terminal |
| Editor | Qualquer | VS Code | Autocompletar para módulo crypto |
Conhecimentos recomendados: JavaScript moderno (async/await, módulos), conceitos básicos de criptografia assimétrica (par de chaves pública/privada) e familiaridade com o terminal. Não é necessário conhecer a matemática das curvas elípticas, mas ajuda a compreender os erros de segurança mais comuns.
Verifique a versão instalada com node --version e openssl version. O módulo crypto está disponível em todos os builds padrão do Node.js, mas pode estar ausente em builds minimalistas. Confirme com o seguinte comando: se retornar um número superior a 20, o suporte a curvas elípticas está completo.
node --version
# v22.x ou superior recomendado
openssl version
# OpenSSL 3.x (incluído com Node.js 18+)
node -e "const c = require('crypto'); console.log('Curvas disponíveis:', c.getCurves().length)"
# Curvas disponíveis: 77 (ou número similar)
node -e "
const c = require('crypto');
['prime256v1','secp384r1','secp256k1'].forEach(curve =>
console.log(curve + ':', c.getCurves().includes(curve) ? 'OK' : 'NAO SUPORTADO')
);"
# prime256v1: OK
# secp384r1: OK
# secp256k1: OK
ECDSA vs Ed25519: Comparação de Desempenho e Casos de Uso
Antes de escrever código, perceba as diferenças entre os dois algoritmos. A escolha errada não compromete apenas a performance, pode criar vulnerabilidades de segurança difíceis de detetar. A tabela abaixo compara os quatro algoritmos de assinatura mais usados em Node.js:
| Característica | Ed25519 | ECDSA P-256 | ECDSA P-384 | RSA-2048 |
|---|---|---|---|---|
| Nível de Segurança | ~128-bit | ~128-bit | ~192-bit | ~112-bit |
| Equivalência RSA | RSA-3072 | RSA-3072 | RSA-7680 | Base de comparação |
| Tamanho da Chave Pública | 32 bytes | 64 bytes | 96 bytes | 256 bytes |
| Tamanho da Assinatura | 64 bytes (fixo) | 70-72 bytes (DER) | 96-104 bytes (DER) | 256 bytes |
| Velocidade de Verificação | ~10x mais rápido que RSA | Mais rápido que RSA | Moderado | Base de comparação |
| Determinístico | Sim | Não | Não | Sim (PKCS#1 v1.5) |
| Nonce necessário | Não | Sim (crítico) | Sim (crítico) | Não |
| Uso em JWTs | EdDSA | ES256 | ES384 | RS256 |
| Algoritmo de Hash Interno | SHA-512 (interno) | SHA-256 | SHA-384 | SHA-256 |
| Suporte WebCrypto | Sim | Sim | Sim | Sim |
Use Ed25519 para APIs internas, autenticação SSH, sistemas peer-to-peer e qualquer contexto onde controla ambos os lados da comunicação. É mais simples, mais seguro operacionalmente e mais rápido. Use ECDSA P-256 (ES256) para JWTs públicos, certificados TLS, assinatura de código e interoperabilidade com sistemas que exigem curvas NIST, como muitas bibliotecas Java e Go que ainda não suportam Ed25519. Use ECDSA P-384 em contextos que exigem segurança de 192-bit, como infraestruturas governamentais ou sistemas de saúde com requisitos regulatórios específicos.
Para a maioria dos projetos Node.js novos em 2026, Ed25519 é a escolha padrão. Para JWTs compatíveis com bibliotecas externas, ECDSA P-256 continua a ser a opção mais interoperável. Nunca use RSA-2048 para novas implementações: oferece apenas 112-bit de segurança, abaixo do mínimo de 128-bit recomendado pelo NIST para sistemas com vida útil superior a 2030.
Passo 1 e 2: Estrutura do Projeto e Configuração Inicial
Crie a estrutura de diretórios do projeto antes de escrever qualquer código de criptografia. Uma organização clara evita misturar chaves de produção com chaves de teste, um erro que frequentemente expõe chaves privadas sensíveis em repositórios git.
# Passo 1: Criar o projeto
mkdir digital-signatures-nodejs
cd digital-signatures-nodejs
npm init -y
mkdir keys signatures utils
# Estrutura final do projeto:
# digital-signatures-nodejs/
# ├── keys/ # chaves PEM (NUNCA commitar para git)
# ├── signatures/ # ficheiros de assinatura exportados
# ├── utils/ # funções auxiliares reutilizáveis
# ├── ecdsa.js # implementação ECDSA
# ├── ed25519.js # implementação Ed25519
# ├── jwt-sign.js # assinatura de JWTs com ES256
# ├── api.js # API Express de verificação
# └── package.json
Configure o .gitignore imediatamente. Chaves privadas não devem nunca entrar no repositório git, mesmo em repositórios privados. Uma única exposição é suficiente para comprometer toda a infraestrutura de assinaturas.
# Passo 2: Criar .gitignore
cat > .gitignore << 'EOF'
keys/
*.pem
*.key
*.p8
node_modules/
.env
signatures/
EOF
# Instalar dependências opcionais para passos avançados
npm install jsonwebtoken express
# [email protected]: assinar JWTs com ES256 e EdDSA
# [email protected]: API de verificação de assinaturas no Passo 12
Adicione ao package.json o campo "type": "module" para usar sintaxe ESM moderna ou mantenha sem o campo e use require() como nos exemplos seguintes (CommonJS). Os exemplos deste tutorial usam require() para máxima compatibilidade com Node.js 18+.
Confirme que o ambiente está pronto com um teste rápido de geração de par de chaves:
node -e "
const crypto = require('crypto');
const { privateKey, publicKey } = crypto.generateKeyPairSync('ed25519', {
privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
publicKeyEncoding: { type: 'spki', format: 'pem' }
});
console.log('Ed25519 OK - Chave pública gerada em', publicKey.length, 'bytes de PEM');
"
# Ed25519 OK - Chave pública gerada em 119 bytes de PEM
Passo 3 e 4: Gerar Par de Chaves ECDSA P-256 e Guardar em PEM
O Node.js usa o nome interno prime256v1 para a curva NIST P-256. Este nome vem do registo OID do OpenSSL. Nos padrões JWT e TLS, a mesma curva aparece como P-256 ou secp256r1. São nomes diferentes para o mesmo objeto matemático. Não confunda com secp256k1, que é a curva usada em Bitcoin e Ethereum, com propriedades diferentes.
// ecdsa.js
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
// Passo 3: Gerar par de chaves ECDSA P-256
function gerarChavesECDSA(curva = 'prime256v1') {
const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', {
namedCurve: curva, // 'prime256v1' = P-256, 'secp384r1' = P-384
privateKeyEncoding: {
type: 'pkcs8', // formato padrão para interoperabilidade
format: 'pem'
},
publicKeyEncoding: {
type: 'spki', // SubjectPublicKeyInfo - formato universal
format: 'pem'
}
});
return { privateKey, publicKey };
}
// Passo 4: Guardar chaves em ficheiros PEM com permissões seguras
function guardarChaves(prefixo, privateKey, publicKey) {
const keysDir = path.join(__dirname, 'keys');
if (!fs.existsSync(keysDir)) fs.mkdirSync(keysDir, { recursive: true });
const privPath = path.join(keysDir, `${prefixo}-private.pem`);
const pubPath = path.join(keysDir, `${prefixo}-public.pem`);
// mode 0o600: apenas o dono pode ler - obrigatório para chaves privadas
fs.writeFileSync(privPath, privateKey, { mode: 0o600 });
fs.writeFileSync(pubPath, publicKey);
console.log(`Chave privada guardada: ${privPath}`);
console.log(`Chave pública guardada: ${pubPath}`);
return { privPath, pubPath };
}
// Executar
const { privateKey, publicKey } = gerarChavesECDSA('prime256v1');
guardarChaves('ecdsa-p256', privateKey, publicKey);
console.log('\n--- Chave Pública ECDSA P-256 (PEM) ---');
console.log(publicKey);
// -----BEGIN PUBLIC KEY-----
// MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE... (88 bytes em base64)
// -----END PUBLIC KEY-----
module.exports = { gerarChavesECDSA, guardarChaves };
Execute com node ecdsa.js. O ficheiro keys/ecdsa-p256-public.pem pode ser partilhado livremente. O ficheiro keys/ecdsa-p256-private.pem é a chave do reino: proteja-o com permissões 600 e nunca o inclua em repositórios ou logs.
A diferença entre os formatos pkcs8 e sec1 para chaves privadas EC é relevante para interoperabilidade: pkcs8 é compatível com Java, Go, Python e OpenSSL moderno. sec1 é o formato EC nativo mas menos universal. Para qualquer novo projeto, use sempre pkcs8. Para gerar chaves P-384, substitua apenas o argumento: gerarChavesECDSA('secp384r1').
Passo 5 e 6: Assinar e Verificar Dados com ECDSA em Node.js
Com o par de chaves gerado, implemente as funções de assinatura e verificação. O Node.js oferece duas APIs: a API de baixo nível com crypto.createSign()/crypto.createVerify() e a API de alto nível com crypto.sign()/crypto.verify(). Use sempre a API de alto nível em código novo, pois é mais simples e menos propensa a erros de implementação.
// utils/ecdsa-ops.js
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
// Passo 5: Assinar dados com ECDSA P-256 + SHA-256
function assinarECDSA(dados, caminhoChavePrivada) {
const privateKeyPem = fs.readFileSync(caminhoChavePrivada, 'utf8');
const assinatura = crypto.sign(
'sha256', // hash: sha256 para P-256, sha384 para P-384
Buffer.from(dados, 'utf8'), // dados como Buffer (encoding explícito!)
{
key: privateKeyPem,
dsaEncoding: 'der' // DER: formato padrão, tamanho variável 70-72 bytes
// Use 'ieee-p1363' para WebCrypto/JOSE: tamanho fixo 64 bytes para P-256
}
);
console.log('Assinatura ECDSA (base64):', assinatura.toString('base64'));
console.log('Tamanho:', assinatura.length, 'bytes (DER, varia 70-72)');
return assinatura;
}
// Passo 6: Verificar assinatura ECDSA
function verificarECDSA(dados, assinatura, caminhoChavePublica) {
const publicKeyPem = fs.readFileSync(caminhoChavePublica, 'utf8');
const valida = crypto.verify(
'sha256',
Buffer.from(dados, 'utf8'), // MESMO encoding usado na assinatura
{
key: publicKeyPem,
dsaEncoding: 'der' // MESMO dsaEncoding usado na assinatura
},
assinatura
);
console.log('Assinatura válida:', valida);
return valida;
}
// Demonstração
const mensagem = 'Pagamento de 250 EUR para IBAN PT50000201231234567890154';
const keysDir = path.join(__dirname, '..', 'keys');
const privKey = path.join(keysDir, 'ecdsa-p256-private.pem');
const pubKey = path.join(keysDir, 'ecdsa-p256-public.pem');
const sig = assinarECDSA(mensagem, privKey);
console.log('\n--- Verificar mensagem original ---');
verificarECDSA(mensagem, sig, pubKey); // true
console.log('\n--- Verificar mensagem adulterada ---');
verificarECDSA('Pagamento de 999 EUR para IBAN PT50000201231234567890154', sig, pubKey); // false
module.exports = { assinarECDSA, verificarECDSA };
Execute com node utils/ecdsa-ops.js. A assinatura tem entre 70 e 72 bytes em formato DER para ECDSA P-256. A variação de tamanho acontece porque os valores r e s da assinatura são inteiros de tamanho variável na codificação ASN.1: quando o bit mais significativo é 1, o DER adiciona um byte 0x00 de padding para evitar interpretação como número negativo.
A verificação com dados adulterados retorna false sem lançar exceção. Nunca ignore o valor de retorno de crypto.verify(): false significa assinatura inválida, não um erro de execução. Tratar apenas exceções e ignorar o retorno booleano é um erro de segurança comum que pode deixar passar assinaturas inválidas.
Passo 7 e 8: Implementar Ed25519 com Assinaturas Determinísticas
Ed25519 tem uma API ligeiramente diferente de ECDSA no Node.js. A diferença mais importante: o parâmetro do algoritmo em crypto.sign() e crypto.verify() deve ser null ou undefined. O Node.js determina o esquema de assinatura a partir do tipo da chave, não de um parâmetro explícito. Passar 'sha256' com uma chave Ed25519 gera o erro ERR_CRYPTO_INVALID_DIGEST.
// ed25519.js
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
// Passo 7: Gerar par de chaves Ed25519
function gerarChavesEd25519() {
const { privateKey, publicKey } = crypto.generateKeyPairSync('ed25519', {
privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
publicKeyEncoding: { type: 'spki', format: 'pem' }
});
const keysDir = path.join(__dirname, 'keys');
if (!fs.existsSync(keysDir)) fs.mkdirSync(keysDir, { recursive: true });
fs.writeFileSync(path.join(keysDir, 'ed25519-private.pem'), privateKey, { mode: 0o600 });
fs.writeFileSync(path.join(keysDir, 'ed25519-public.pem'), publicKey);
console.log('Par de chaves Ed25519 gerado e guardado.');
return { privateKey, publicKey };
}
// Passo 8a: Assinar com Ed25519
// REGRA CRÍTICA: o primeiro parâmetro DEVE ser null (não 'sha256' ou qualquer hash)
function assinarEd25519(dados, privateKeyPem) {
const assinatura = crypto.sign(
null, // null obrigatório - Ed25519 usa SHA-512 internamente
Buffer.from(dados, 'utf8'),
privateKeyPem
);
console.log('Assinatura Ed25519 (base64url):', assinatura.toString('base64url'));
console.log('Tamanho:', assinatura.length, 'bytes (sempre 64 bytes)');
return assinatura;
}
// Passo 8b: Verificar com Ed25519
function verificarEd25519(dados, assinatura, publicKeyPem) {
const valida = crypto.verify(
null, // null obrigatório para Ed25519
Buffer.from(dados, 'utf8'),
publicKeyPem,
assinatura
);
console.log('Assinatura Ed25519 válida:', valida);
return valida;
}
// Demonstração completa
const { privateKey, publicKey } = gerarChavesEd25519();
const mensagem = 'Transacao #TX-2026-001 | Valor: 1500 EUR | Timestamp: 1750000000';
console.log('\n--- Assinar com Ed25519 ---');
const sig = assinarEd25519(mensagem, privateKey);
console.log('\n--- Verificar assinatura original ---');
verificarEd25519(mensagem, sig, publicKey); // true
console.log('\n--- Verificar mensagem adulterada ---');
verificarEd25519(mensagem + ' (adulterado)', sig, publicKey); // false
// Demonstrar determinismo: a mesma mensagem gera sempre a mesma assinatura
const sig2 = assinarEd25519(mensagem, privateKey);
console.log('\n--- Confirmar determinismo ---');
console.log('Assinaturas idênticas:', sig.equals(sig2)); // true (impossível em ECDSA)
module.exports = { gerarChavesEd25519, assinarEd25519, verificarEd25519 };
Execute com node ed25519.js. A saída confirma que Ed25519 produz sempre exatamente 64 bytes de assinatura, independentemente do tamanho da mensagem. A linha "Assinaturas idênticas: true" demonstra o determinismo: dado o mesmo par chave privada e mensagem, Ed25519 gera sempre o mesmo resultado.
Este determinismo é uma vantagem de segurança importante. Em ECDSA, se o gerador de números aleatórios produzir o mesmo nonce k duas vezes (por bug, seed fraco ou falha de entropia), a chave privada fica exposta. Ed25519 elimina completamente esta classe de vulnerabilidade porque o nonce é derivado deterministicamente da mensagem e da chave privada usando SHA-512.
Passo 9: Codificação DER vs IEEE P1363 para Interoperabilidade
A codificação da assinatura ECDSA é a fonte de um dos erros mais frequentes na integração com outros sistemas. O Node.js usa DER por padrão, mas muitas bibliotecas, incluindo a WebCrypto API do browser, JOSE e algumas implementações Java, esperam o formato IEEE P1363. Compreender a diferença evita horas de debugging em produção.
Formato DER: a assinatura é uma estrutura ASN.1 que codifica os dois inteiros r e s com cabeçalhos de tipo e comprimento. Para ECDSA P-256, o tamanho varia entre 70 e 72 bytes, dependendo dos valores de r e s. É o formato padrão para TLS, certificados X.509 e OpenSSL.
Formato IEEE P1363: concatenação direta de r || s, com cada valor preenchido com zeros à esquerda até ao tamanho fixo da curva. Para P-256, são sempre 64 bytes (32 + 32). É o formato esperado pela WebCrypto API e por implementações JOSE.
// utils/encoding-demo.js
const crypto = require('crypto');
function demonstrarFormatos() {
// Gerar par de chaves temporário para o exemplo
const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', {
namedCurve: 'prime256v1',
privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
publicKeyEncoding: { type: 'spki', format: 'pem' }
});
const dados = Buffer.from('mensagem de teste para comparar formatos', 'utf8');
// Assinatura em DER (padrão Node.js)
const sigDer = crypto.sign('sha256', dados, { key: privateKey, dsaEncoding: 'der' });
console.log('DER:', sigDer.length, 'bytes (70-72 bytes para P-256)');
console.log('DER hex (início):', sigDer.subarray(0, 4).toString('hex'));
// 30 44 02 20 ... (0x30 = SEQUENCE, 0x44 = comprimento, 0x02 = INTEGER, 0x20 = 32 bytes)
// Assinatura em IEEE P1363 (tamanho fixo)
const sigP1363 = crypto.sign('sha256', dados, { key: privateKey, dsaEncoding: 'ieee-p1363' });
console.log('IEEE P1363:', sigP1363.length, 'bytes (sempre 64 bytes para P-256)');
// Verificação: usar SEMPRE o mesmo dsaEncoding
const vDer = crypto.verify('sha256', dados, { key: publicKey, dsaEncoding: 'der' }, sigDer);
const vP1363 = crypto.verify('sha256', dados, { key: publicKey, dsaEncoding: 'ieee-p1363' }, sigP1363);
const vMixed = crypto.verify('sha256', dados, { key: publicKey, dsaEncoding: 'ieee-p1363' }, sigDer);
console.log('\nResultados:');
console.log('DER com DER:', vDer); // true
console.log('P1363 com P1363:', vP1363); // true
console.log('DER com P1363 (errado):', vMixed); // false - encoding misto falha!
}
demonstrarFormatos();
A linha "DER com P1363 (errado): false" demonstra o problema real. Quando um serviço Node.js assina com o padrão DER e envia para um browser que usa WebCrypto (que espera IEEE P1363), a verificação falha silenciosamente, retornando apenas false. Regra prática: use dsaEncoding: 'ieee-p1363' para todas as integrações com WebCrypto no browser ou com JOSE/JWT. Use der (padrão) para TLS, certificados e OpenSSL.
Passo 10: Assinar JWTs com ES256 (ECDSA + SHA-256)
O algoritmo ES256 definido na RFC 7518 usa ECDSA P-256 com SHA-256 para assinar JSON Web Tokens. É a alternativa mais segura ao RS256 (RSA) para JWTs: chaves menores, assinaturas mais pequenas e verificação mais rápida. A biblioteca jsonwebtoken versão 9.0+ suporta também o algoritmo EdDSA (Ed25519) nativamente.
// jwt-sign.js
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
// Gerar chaves ECDSA P-256 específicas para JWT
const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', {
namedCurve: 'prime256v1',
privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
publicKeyEncoding: { type: 'spki', format: 'pem' }
});
const keysDir = path.join(__dirname, 'keys');
if (!fs.existsSync(keysDir)) fs.mkdirSync(keysDir);
fs.writeFileSync(path.join(keysDir, 'jwt-private.pem'), privateKey, { mode: 0o600 });
fs.writeFileSync(path.join(keysDir, 'jwt-public.pem'), publicKey);
// Passo 10a: Emitir JWT com ES256
function emitirToken(payload, expiracaoSegundos = 3600) {
const token = jwt.sign(payload, privateKey, {
algorithm: 'ES256', // ECDSA P-256 + SHA-256 (ES256 em maiúsculas!)
expiresIn: expiracaoSegundos,
issuer: 'api.shattered.io',
audience: 'api-clients'
});
// Inspecionar o token
const [header, body] = token.split('.').slice(0, 2)
.map(p => JSON.parse(Buffer.from(p, 'base64url').toString('utf8')));
console.log('Header JWT:', JSON.stringify(header));
// {"alg":"ES256","typ":"JWT"}
console.log('Payload JWT:', JSON.stringify(body));
console.log('Token (primeiros 80 chars):', token.substring(0, 80) + '...');
return token;
}
// Passo 10b: Verificar JWT com validação explícita de algoritmo
function verificarToken(token) {
try {
const payload = jwt.verify(token, publicKey, {
algorithms: ['ES256'], // OBRIGATÓRIO: previne ataque alg:none e downgrade
issuer: 'api.shattered.io',
audience: 'api-clients'
});
console.log('JWT valido. Utilizador:', payload.sub, '| Expira:', new Date(payload.exp * 1000).toISOString());
return payload;
} catch (err) {
console.error('JWT invalido:', err.message);
return null;
}
}
// Demonstração
console.log('\n--- Emitir token ES256 ---');
const token = emitirToken({ sub: 'user-12345', role: 'admin', email: '[email protected]' });
console.log('\n--- Verificar token original ---');
verificarToken(token);
console.log('\n--- Verificar token adulterado ---');
const tokenAdulterado = token.slice(0, -5) + 'XXXXX';
verificarToken(tokenAdulterado);
module.exports = { emitirToken, verificarToken };
Execute com node jwt-sign.js. O header confirma "alg":"ES256". A verificação do token adulterado imprime "JWT invalido: invalid signature".
O campo algorithms: ['ES256'] na verificação é obrigatório. Omiti-lo permite que um atacante altere o header do JWT para "alg":"none" e submeta um token sem assinatura válido. Este ataque, documentado desde 2015, ainda afeta implementações que omitem a validação explícita do algoritmo. Para usar Ed25519 em JWTs, substitua 'ES256' por 'EdDSA' e gere chaves com crypto.generateKeyPairSync('ed25519').
Passo 11: Gestão de Chaves PEM em Produção
Em produção, as chaves não são geradas em runtime. São geradas uma vez, armazenadas com segurança e carregadas na inicialização da aplicação. Uma estratégia correta inclui rotação periódica, separação entre ambientes (dev/staging/prod) e nunca armazenar chaves privadas em texto simples em sistemas de CI públicos.
// utils/key-manager.js
const crypto = require('crypto');
const fs = require('fs');
// Carregar chave privada de ficheiro ou variável de ambiente
// Em produção: KEY_PRIVATE_B64=base64_da_chave_pem
function carregarChavePrivada(caminhoOuEnvVar) {
if (process.env[caminhoOuEnvVar]) {
// Variável de ambiente em base64 (para Kubernetes secrets, AWS SSM, etc.)
const pem = Buffer.from(process.env[caminhoOuEnvVar], 'base64').toString('utf8');
return crypto.createPrivateKey(pem);
}
// Ficheiro local (desenvolvimento e staging)
const pem = fs.readFileSync(caminhoOuEnvVar, 'utf8');
return crypto.createPrivateKey(pem);
}
function carregarChavePublica(caminhoOuEnvVar) {
if (process.env[caminhoOuEnvVar]) {
const pem = Buffer.from(process.env[caminhoOuEnvVar], 'base64').toString('utf8');
return crypto.createPublicKey(pem);
}
const pem = fs.readFileSync(caminhoOuEnvVar, 'utf8');
return crypto.createPublicKey(pem);
}
// Exportar chave pública em múltiplos formatos
function exportarChavePublica(pubKeyObj) {
const pem = pubKeyObj.export({ type: 'spki', format: 'pem' });
const jwk = pubKeyObj.export({ format: 'jwk' });
console.log('Tipo de chave:', pubKeyObj.asymmetricKeyType); // 'ec' ou 'ed25519'
console.log('Detalhes:', pubKeyObj.asymmetricKeyDetails); // { namedCurve: 'prime256v1' }
console.log('JWK:', JSON.stringify(jwk, null, 2));
// Para ECDSA P-256:
// { "kty": "EC", "crv": "P-256", "x": "...", "y": "..." }
// Para Ed25519:
// { "kty": "OKP", "crv": "Ed25519", "x": "..." }
return { pem, jwk };
}
module.exports = { carregarChavePrivada, carregarChavePublica, exportarChavePublica };
O formato JWK (JSON Web Key) é particularmente útil para expor chaves públicas via endpoint JWKS (JSON Web Key Set), o padrão OAuth 2.0 que permite a serviços externos verificar JWTs sem receber a chave pública por canal privado. O endpoint convencional é /.well-known/jwks.json. Para rotação de chaves sem downtime, mantenha múltiplas chaves no JWKS com campos kid diferentes e inclua o kid no header de cada JWT emitido.
Para produção com requisitos de segurança elevados, considere AWS KMS ou Azure Key Vault: as operações de assinatura ocorrem dentro do HSM (Hardware Security Module) sem nunca exportar a chave privada. O Node.js suporta esta integração através dos respetivos SDKs, sem alterações ao código de verificação.
Passo 12: API Express Completa de Verificação de Assinaturas
O projeto completo culmina numa API Express que expõe quatro endpoints: assinar dados, verificar assinaturas, publicar a chave pública em formato JWK, e um health check. Esta API demonstra como integrar assinaturas digitais numa aplicação real com boas práticas de segurança.
// api.js - API Express de assinaturas digitais com Ed25519
const express = require('express');
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const app = express();
app.use(express.json({ limit: '10kb' })); // limitar tamanho do body
// Carregar chaves Ed25519 na inicialização (uma vez, não por request)
const keysDir = path.join(__dirname, 'keys');
let privKey, pubKey, pubKeyObj;
try {
privKey = fs.readFileSync(path.join(keysDir, 'ed25519-private.pem'), 'utf8');
pubKey = fs.readFileSync(path.join(keysDir, 'ed25519-public.pem'), 'utf8');
pubKeyObj = crypto.createPublicKey(pubKey); // parse uma vez para reutilizar
console.log('Chaves Ed25519 carregadas. Pronto.');
} catch (err) {
console.error('Erro ao carregar chaves:', err.message);
console.error('Execute primeiro: node ed25519.js');
process.exit(1);
}
// POST /sign - assinar payload com Ed25519
app.post('/sign', (req, res) => {
const { dados } = req.body;
if (!dados || typeof dados !== 'string' || dados.length > 4096) {
return res.status(400).json({ erro: 'Campo "dados" obrigatorio (string, max 4096 chars)' });
}
const assinatura = crypto.sign(null, Buffer.from(dados, 'utf8'), privKey);
res.json({
dados,
assinatura: assinatura.toString('base64url'), // base64url sem padding
algoritmo: 'Ed25519',
tamanho_bytes: assinatura.length // sempre 64
});
});
// POST /verify - verificar assinatura Ed25519
app.post('/verify', (req, res) => {
const { dados, assinatura } = req.body;
if (!dados || !assinatura) {
return res.status(400).json({ erro: 'Campos "dados" e "assinatura" obrigatorios' });
}
let sigBuffer;
try {
sigBuffer = Buffer.from(assinatura, 'base64url');
if (sigBuffer.length !== 64) throw new Error('Tamanho invalido');
} catch {
return res.status(400).json({ erro: 'Assinatura base64url invalida (deve ter 64 bytes)' });
}
// crypto.verify() usa comparacao em tempo constante internamente
const valida = crypto.verify(null, Buffer.from(dados, 'utf8'), pubKeyObj, sigBuffer);
res.json({ valida, algoritmo: 'Ed25519' });
});
// GET /public-key - chave publica em formato JWKS
app.get('/public-key', (_req, res) => {
const jwk = pubKeyObj.export({ format: 'jwk' });
res.json({
keys: [{ ...jwk, use: 'sig', kid: 'ed25519-2026-01', alg: 'EdDSA' }]
});
});
// GET /health
app.get('/health', (_req, res) => res.json({ estado: 'ok', algoritmo: 'Ed25519' }));
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`API de assinaturas em http://localhost:${PORT}`);
console.log('Endpoints: POST /sign | POST /verify | GET /public-key | GET /health');
});
module.exports = app;
Execute com node api.js e teste com curl:
# Assinar dados
curl -s -X POST http://localhost:3000/sign \
-H 'Content-Type: application/json' \
-d '{"dados":"Transacao TX-001: 500 EUR"}' | python3 -m json.tool
# Resposta esperada:
# {
# "dados": "Transacao TX-001: 500 EUR",
# "assinatura": "abc123...xyz" (86 caracteres base64url para 64 bytes Ed25519),
# "algoritmo": "Ed25519",
# "tamanho_bytes": 64
# }
# Verificar (substituir ASSINATURA pelo valor retornado pelo /sign)
curl -s -X POST http://localhost:3000/verify \
-H 'Content-Type: application/json' \
-d '{"dados":"Transacao TX-001: 500 EUR","assinatura":"ASSINATURA"}' | python3 -m json.tool
# Obter chave pública em JWKS
curl -s http://localhost:3000/public-key | python3 -m json.tool
5 Erros Comuns que Comprometem Implementações de Assinaturas Digitais
A maioria das vulnerabilidades em sistemas de assinaturas digitais não vem de fraquezas matemáticas, mas de erros de implementação. Estes são os cinco erros mais frequentes em código Node.js e como evitá-los:
Erro 1: Passar 'sha256' como algoritmo para chaves Ed25519. crypto.sign('sha256', dados, ed25519Key) lança Error [ERR_CRYPTO_INVALID_DIGEST]: Invalid digest algorithm sha256 for key type ed25519. Ed25519 incorpora SHA-512 internamente e não permite seleção de hash pelo utilizador. Use sempre crypto.sign(null, ...) e crypto.verify(null, ...) com chaves Ed25519. ECDSA aceita um hash como primeiro argumento; Ed25519 não.
Erro 2: Reutilizar nonce em implementações manuais de ECDSA. O Node.js usa OpenSSL, que gera nonces seguros automaticamente. Mas se alguma vez implementar ECDSA manualmente (por exemplo, em código de blockchain) com um nonce constante ou um PRNG fraco, a chave privada pode ser extraída matematicamente de apenas duas assinaturas. Este ataque extraiu a chave de consolas PlayStation 3 em 2010. Soluções: confie no módulo crypto nativo para ECDSA, ou migre para Ed25519 que é determinístico por design.
Erro 3: Misturar formatos DER e IEEE P1363. Uma assinatura ECDSA gerada com dsaEncoding: 'der' retorna false quando verificada com dsaEncoding: 'ieee-p1363', mesmo com chave e dados corretos. Documente sempre o formato de codificação como parte da especificação da API. A regra simples: DER para OpenSSL/TLS/certificados, IEEE P1363 para WebCrypto e JOSE.
Erro 4: Encoding inconsistente entre assinatura e verificação. Se assinou com Buffer.from(dados) (UTF-8 por padrão) mas verifica com Buffer.from(dados, 'base64'), a verificação falha. O encoding dos dados deve ser idêntico em ambos os lados. Documente o encoding como parte do protocolo e teste explicitamente com dados que contenham caracteres não-ASCII (acentos, emoji) para detetar problemas de encoding precocemente.
Erro 5: Aceitar a chave pública no mesmo payload que os dados e a assinatura. Verificar uma assinatura com a chave pública errada retorna false, mas código que aceita chaves públicas de fontes não autenticadas pode ser enganado por um atacante que forneça a sua própria chave. A chave pública usada na verificação deve vir sempre de uma fonte autenticada: certificado X.509, endpoint JWKS autenticado ou configuração local imutável. Nunca aceite chaves públicas no mesmo canal não autenticado que transporta os dados assinados.
Resolução de Problemas: 8 Erros Frequentes em Node.js
Esta secção cobre as mensagens de erro de runtime mais comuns e como resolver cada uma.
Problema 1: Error: error:0909006C:PEM routines:get_name:no start line
O ficheiro PEM está corrompido, truncado ou contém caracteres extra (BOM, espaços antes do cabeçalho). Verifique com openssl pkey -in private.pem -noout. O ficheiro deve começar exatamente com -----BEGIN PRIVATE KEY----- sem qualquer carácter anterior. Se o ficheiro foi criado no Windows, podem existir carriage returns (\r) que corrompem a leitura. Regenere as chaves em ambiente limpo.
Problema 2: Error [ERR_CRYPTO_INVALID_DIGEST]: Invalid digest algorithm sha256 for key type ed25519
Passou 'sha256' como primeiro argumento de crypto.sign() com uma chave Ed25519. Mude para null. Esta é a diferença mais comum entre desenvolvedores que migram de ECDSA para Ed25519.
Problema 3: JsonWebTokenError: invalid algorithm
O algoritmo passado à biblioteca jsonwebtoken está em formato errado. Use 'ES256' em maiúsculas, não 'es256'. Para Ed25519, use 'EdDSA', não 'ed25519'. A biblioteca é sensível ao case dos nomes de algoritmo.
Problema 4: crypto.verify() retorna false com assinatura aparentemente correta
Causas por ordem de frequência: (1) encoding diferente entre assinatura e verificação (DER vs P1363), (2) encoding dos dados diferente (UTF-8 vs base64), (3) assinatura truncada na transmissão HTTP porque foi enviada como base64 com + e / que foram alterados por URL encoding, use base64url em vez de base64 para URLs, (4) chave pública diferente da usada na assinatura.
Problema 5: Error: error:06089094:digital envelope routines:EVP_DigestInit_ex:invalid digest
Usando SHA-1 ou MD5 com ECDSA. O OpenSSL 3.x, incluído no Node.js 18+, desativou estes algoritmos por padrão. Use 'sha256' para P-256 ou 'sha384' para P-384. Se o código usa SHA-1 por legado, a migração para SHA-256 é obrigatória.
Problema 6: Chave gerada com formato sec1 não funciona com jsonwebtoken
A biblioteca espera chaves privadas EC em formato PKCS8. Se gerou com type: 'sec1', converta: openssl pkcs8 -topk8 -nocrypt -in sec1-key.pem -out pkcs8-key.pem. Para evitar este problema, use sempre type: 'pkcs8' na geração.
Problema 7: Performance lenta com muitas verificações por segundo
Criar o objeto de chave (crypto.createPublicKey()) a cada verificação tem overhead de parse PEM. Em código de alto throughput, crie o objeto uma vez na inicialização e reutilize-o. O parse PEM adiciona aproximadamente 0,1ms por operação: com 10.000 verificações/segundo, são 1 segundo de overhead por segundo totalmente evitável.
Problema 8: ERR_OSSL_EVP_UNSUPPORTED no Node.js 17 ou superior
O OpenSSL 3.0 incluído a partir do Node.js 17 desativou algoritmos legados. A variável de ambiente NODE_OPTIONS=--openssl-legacy-provider é uma solução temporária de migração, nunca de produção. A solução correta é atualizar o código para usar algoritmos modernos (SHA-256, SHA-384, AES-256-GCM).
Dicas Avançadas para Implementações em Produção
Comparação em tempo constante para verificação de tokens. A função crypto.verify() do Node.js usa comparação em tempo constante internamente, o que protege contra ataques de timing. Se implementar verificação personalizada que compara buffers byte a byte, use sempre crypto.timingSafeEqual(a, b) em vez de a.equals(b) ou operadores de igualdade normais. A comparação normal termina mais cedo quando encontra o primeiro byte diferente, revelando informação sobre o conteúdo esperado.
Rotação de chaves sem downtime com JWKS. Publique várias chaves públicas no endpoint JWKS com diferentes valores de kid (Key ID). O header do JWT inclui o kid que identifica qual chave usar na verificação. Quando rodar chaves, adicione a nova chave ao JWKS antes de começar a emitir tokens com ela. Mantenha a chave antiga no JWKS durante o período de expiração máxima dos tokens existentes (tipicamente 24 a 48 horas).
Verificação assíncrona com WebCrypto API. O Node.js expõe a WebCrypto API através de require('crypto').webcrypto ou globalThis.crypto no Node.js 19+. Esta API é assíncrona e compatível com browsers, ideal para servidores com alto volume de verificações que não devem bloquear o event loop. Use webcrypto.subtle.verify({name: 'Ed25519'}, ...) para Ed25519 ou {name: 'ECDSA', hash: 'SHA-256'} para P-256.
Assinaturas sobre streams para ficheiros grandes. Para assinar ficheiros grandes sem carregar tudo na memória, use a API de stream: const sign = crypto.createSign('sha256'), seguido de sign.update(chunk) para cada fragmento do ficheiro, e finalmente sign.sign(privateKey). Esta abordagem processa o conteúdo incrementalmente com footprint de memória constante, independentemente do tamanho do ficheiro. Ed25519 não suporta streaming nativo, por isso para ficheiros grandes com Ed25519 calcule primeiro o hash SHA-512 do ficheiro e assine o hash.
Contexto de assinatura Ed25519 no Node.js 26+. O Node.js 26.0.0 adicionou suporte ao parâmetro de contexto para Ed25519, definido na RFC 8032. Este parâmetro vincula a assinatura a um contexto específico (por exemplo, 'autenticacao-api-v2'), impedindo que assinaturas válidas de um contexto sejam reutilizadas noutro. Esta funcionalidade é relevante para sistemas multi-contexto onde o mesmo par de chaves é usado em diferentes protocolos ou versões de API.
Conformidade com Padrões NIST e RFC em 2026
Para implementações em ambientes regulados, é importante alinhar as escolhas de algoritmo com os padrões oficiais em vigor. O NIST FIPS 186-5, publicado em fevereiro de 2023, é o documento de referência atual para assinaturas digitais aprovadas pelo governo americano. Inclui ECDSA com curvas NIST (P-256, P-384, P-521) e EdDSA incluindo Ed25519 e Ed448. RSA-1024 e DSA foram descontinuados; RSA-2048 ainda é permitido mas desencorajado para novas implementações.
A WebCrypto API W3C, especificada em w3.org/TR/WebCryptoAPI, define a interface padrão para operações criptográficas em browsers e ambientes JavaScript. O Node.js implementa esta especificação através de crypto.webcrypto, garantindo código portável entre servidor e cliente.
Para implementações pós-quânticas futuras, o NIST selecionou em 2024 os algoritmos ML-DSA (baseado em Dilithium) e SLH-DSA para assinaturas resistentes a computadores quânticos. Node.js ainda não suporta estes algoritmos nativamente em 2026, mas a migração será necessária em contextos de longa duração. ECDSA e Ed25519 são adequados para todos os casos de uso atuais.
FAQ: Perguntas Frequentes sobre Assinaturas Digitais em Node.js
Ed25519 é compatível com todos os sistemas que verificam JWTs?
Não. O algoritmo EdDSA para JWTs é suportado por jsonwebtoken 9.0+, jose 4.0+, e bibliotecas modernas em Go, Python e Java. Sistemas mais antigos ou algumas implementações empresariais suportam apenas RS256 e ES256. Se a interoperabilidade com sistemas externos é necessária, verifique o suporte a EdDSA antes de adotar. Para sistemas internos onde controla ambos os lados, Ed25519 é sempre a escolha preferível.
Qual a diferença entre assinar e encriptar dados?
Assinar e encriptar são operações distintas com objetivos diferentes. Assinar garante autenticidade e integridade: qualquer pessoa com a chave pública pode verificar a origem dos dados. Encriptar garante confidencialidade: apenas o titular da chave privada pode decifrar. É possível fazer ambos em simultâneo (assinar primeiro, depois encriptar), mas são passos independentes. Para proteger dados em repouso, use AES-256. Para autenticar a origem, use assinaturas digitais.
Quanto tempo demora uma operação de assinatura Ed25519 em Node.js?
Em hardware moderno (CPU x86-64), Ed25519 assina em menos de 0,1 milissegundos e verifica em menos de 0,2 milissegundos. Isto permite mais de 10.000 operações por segundo num único thread Node.js. RSA-2048 assina em cerca de 1 milissegundo e verifica em cerca de 0,05 milissegundos (verificação mais rápida porque usa o expoente público pequeno). ECDSA P-256 fica entre Ed25519 e RSA em performance de assinatura. Para APIs REST com centenas de pedidos por segundo, qualquer um dos três é suficientemente rápido; a diferença torna-se relevante em sistemas que processam milhares de assinaturas por segundo.
É seguro expor a chave pública num endpoint público?
Sim. A chave pública pode e deve ser publicada. É matematicamente impossível derivar a chave privada a partir da chave pública com Ed25519 ou ECDSA P-256, mesmo com os computadores mais potentes disponíveis em 2026. O endpoint JWKS (/.well-known/jwks.json) é um padrão OAuth 2.0 concebido precisamente para publicar chaves públicas. O que deve ser protegido com cuidado extremo é a chave privada, que nunca deve sair do servidor onde é usada.
Devo usar ECDSA P-256 ou P-384 para novos projetos?
P-256 oferece 128-bit de segurança, equivalente a RSA-3072, e é suficiente para a vasta maioria das aplicações até pelo menos 2030 segundo estimativas NIST. P-384 oferece 192-bit de segurança e é exigido por regulamentações de alguns setores (defesa e saúde em certas jurisdições). Para APIs e JWTs em projetos comerciais normais, P-256 (ES256) é a escolha padrão pela melhor performance e compatibilidade universal. Para ambientes com requisitos regulatórios específicos de 192-bit, use P-384 (ES384). Para novos projetos sem restrições de interoperabilidade, Ed25519 supera ambos.
Como verificar assinaturas de webhooks de serviços como Stripe ou GitHub?
Stripe e GitHub usam HMAC-SHA256, não ECDSA ou Ed25519, para assinar webhooks. O processo usa uma chave secreta partilhada entre o serviço e o seu servidor, não um par de chaves assimétrico. O Node.js verifica com crypto.createHmac('sha256', secret).update(payload).digest('hex') e comparação em tempo constante com crypto.timingSafeEqual(). Consulte o artigo de HMAC-SHA256 em Node.js para um guia completo sobre verificação de webhooks com HMAC.
As assinaturas digitais são vulneráveis à computação quântica?
Sim. Um computador quântico suficientemente potente poderia quebrar ECDSA e Ed25519 usando o algoritmo de Shor. No entanto, computadores quânticos com capacidade criptograficamente relevante não existem em 2026 e as estimativas mais conservadoras apontam para 2030 ou posterior. O NIST está a padronizar algoritmos pós-quânticos (ML-DSA, SLH-DSA) que resistem a ataques quânticos. Para novos projetos de longa duração, monitorize a disponibilidade destes algoritmos no OpenSSL e Node.js. Para implementações atuais, ECDSA e Ed25519 são seguros.
Cobertura Relacionada
Para aprofundar os temas abordados neste tutorial, consulte estes recursos do arquivo shattered.io:
- Módulo Crypto do Node.js: 12 Passos, 30 Min [2026] - guia completo ao módulo nativo com hashing, cifragem simétrica e HMAC
- HMAC-SHA256 em Node.js: 10 Passos, 20 Min [2026] - autenticação de mensagens com chave secreta partilhada para webhooks
- Encriptação RSA em Node.js: 11 Passos [2026] - criptografia assimétrica com RSA-2048 e RSA-4096 para comparação com ECDSA
- Autenticação JWT em Node.js: 10 Passos [2026] - tokens de sessão seguros com RS256 e ES256 em aplicações Express
- Encriptação AES-256 em Node.js: 12 Passos [2026] - cifra simétrica de alto desempenho com GCM para dados em repouso
- Autenticação SSH com Chaves Ed25519: 12 Passos [2026] - uso prático de Ed25519 para acesso seguro a servidores remotos
- Criptografia: Guia Completo [2026] - pilar do cluster com todos os recursos sobre criptografia moderna




