Use a API Web Cryptography (crypto.subtle) para assinar e verificar mensagens, tokens ou artefatos no navegador usando chaves assimétricas resistentes a ataques quânticos (ML-DSA) ou clássicas ('Ed25519', 'ECDSA'). As assinaturas digitais garantem a integridade de dados e a autenticidade do signatário sem exigir chaves secretas compartilhadas. Para estabelecer secrets compartilhados e criptografar payloads confidenciais, consulte Criptografar dados do lado do cliente com o WebCrypto.
1. Escolher um algoritmo e gerar um par de chaves
O ML-DSA (algoritmo de assinatura digital baseado em rede de módulos, padronizado no NIST FIPS 204) fornece assinaturas digitais pós-quânticas baseadas em rede resistentes a ataques de computadores clássicos e quânticos.
Use 'ML-DSA-65' por padrão para assinatura de uso geral. Escolha 'ML-DSA-44' quando precisar de chaves públicas e assinaturas menores em canais com largura de banda limitada e a segurança de 128 bits for suficiente, ou 'ML-DSA-87' quando uma política de segurança exigir requisitos da política da Categoria 5 do NIST (CNSA 2.0).
| Nome do algoritmo | Nível de segurança e caso de uso | Chave pública ('raw-public') |
Tamanho da assinatura |
|---|---|---|---|
'ML-DSA-44' |
Categoria 2 do NIST (segurança de ~128 bits); chaves e assinaturas menores | 1.312 bytes | 2.420 bytes |
'ML-DSA-65' |
Categoria 3 do NIST (segurança de ~192 bits); padrão recomendado | 1.952 bytes | 3309 bytes |
'ML-DSA-87' |
Categoria 5 do NIST (segurança de ~256 bits); maior margem de segurança | 2.592 bytes | 4.627 bytes |
- Transmita
'ML-DSA-44','ML-DSA-65'ou'ML-DSA-87'como o nome do algoritmo (nomes pré-padronizados, como'Dilithium'ou'CRYSTALS-Dilithium', geramNotSupportedError). - Nem o ML-DSA nem o
'Ed25519'usam um parâmetrohash. - Transmita
['sign', 'verify']ao gerar um par de chaves comgenerateKey():
const signerKeyPair = await crypto.subtle.generateKey(
'ML-DSA-65',
false,
['sign', 'verify'],
);
2. Exportar e importar chaves
'ML-DSA-44', 'ML-DSA-65' e 'ML-DSA-87' rejeitam o formato legado 'raw' em importKey() e exportKey() com um NotSupportedError. Sempre exporte ou importe chaves usando um formato explícito:
'raw-public': bytes de chave pública (1312,1952ou2592bytes). Transmita['verify']como o uso da chave ao importar uma chave pública.'raw-seed': semente de chave privada de 32 bytes para os três algoritmos ML-DSA. Tanto'raw'quanto'raw-private'geramNotSupportedErrorno ML-DSA. Armazene ou transmita chaves privadas usando'raw-seed','pkcs8'(54 bytes) ou'jwk'(kty: 'AKP'). Transmita['sign']como o uso da chave ao importar uma chave privada (transmitir['sign', 'verify']paraimportKey()geraSyntaxError).'spki','pkcs8'e'jwk': compatíveis com os três algoritmos de ML-DSA.
Sempre exporte a chave pública de verificação com 'raw-public' para que os bytes da chave pública possam ser serializados ou compartilhados com os verificadores:
// Export the public key bytes on the signer
const publicKeyBytes = await crypto.subtle.exportKey(
'raw-public',
signerKeyPair.publicKey,
);
// Import the public key bytes on the verifier (usage must be ['verify'] only)
const verifierPublicKey = await crypto.subtle.importKey(
'raw-public',
publicKeyBytes,
'ML-DSA-65',
true,
['verify'],
);
Se você tiver apenas uma CryptoKey de assinatura particular (mesmo com extractable: false) e precisar da CryptoKey pública correspondente, chame crypto.subtle.getPublicKey() com ['verify']:
const publicKey = await crypto.subtle.getPublicKey(
signerKeyPair.privateKey,
['verify'],
);
3. Assinar e verificar payloads
Chame crypto.subtle.sign() com a chave privada para produzir uma assinatura e crypto.subtle.verify() com a chave pública para verificá-la:
const payloadBytes = new TextEncoder().encode(
'{"artifact":"release-v2.4.0.tar.gz","sha256":"9f86d08..."}',
);
const signatureBuffer = await crypto.subtle.sign(
'ML-DSA-65',
signerKeyPair.privateKey,
payloadBytes,
);
const isValid = await crypto.subtle.verify(
'ML-DSA-65',
verifierPublicKey,
signatureBuffer,
payloadBytes,
);
Separação de domínios com ContextParams
Para evitar que uma assinatura criada para uma finalidade seja reproduzida em outra parte do aplicativo ou protocolo que compartilha o mesmo par de chaves, vincule a assinatura a um context de separação de domínio.
Em vez de uma string de nome de algoritmo simples, transmita um dicionário ContextParams ({ name, context }) para sign() e verify(), em que context é um BufferSource (Uint8Array ou ArrayBuffer):
const contextBytes = new TextEncoder().encode('release-manifest-v1');
const signatureBuffer = await crypto.subtle.sign(
{
name: 'ML-DSA-65',
context: contextBytes,
},
signerKeyPair.privateKey,
payloadBytes,
);
const isValid = await crypto.subtle.verify(
{
name: 'ML-DSA-65',
context: contextBytes,
},
verifierPublicKey,
signatureBuffer,
payloadBytes,
);
- Limite de 255 bytes:
contextaceita de0a255bytes (omitircontextequivale a transmitir umnew Uint8Array(0)vazio). Transmitir mais de255bytes rejeita comOperationError. - Correspondência exata necessária:
crypto.subtle.verify()retornatruesomente quando o verificador transmite os mesmos bytescontextusados durantecrypto.subtle.sign().
Estratégias de fallback
Se o destino do Baseline ainda não for compatível com SubtleCrypto.supports() ou ML-DSA, detecte a compatibilidade usando o método estático SubtleCrypto.supports() e use o 'Ed25519' clássico (ou 'ECDSA' com 'P-256' e 'SHA-256' ao segmentar navegadores anteriores à compatibilidade com 'Ed25519'):
- Chame
SubtleCrypto.supports()no construtorSubtleCrypto, não emcrypto.subtle:globalThis.SubtleCrypto?.supports?.('sign', 'ML-DSA-65')é um método estático síncrono que retorna um booleano (crypto.subtle.supportséundefined). Verifique'sign','verify','generateKey','importKey','exportKey'ou'getPublicKey'(ou transmita um dicionárioContextParams, comoSubtleCrypto.supports('sign', { name: 'ML-DSA-65', context })). - Formato de chave no substituto (
'raw'): embora os navegadores compatíveis com o Modern WebCrypto também aceitem'raw-public'em'Ed25519'e'ECDSA', use'raw'em caminhos substitutos destinados a navegadores mais antigos. - Separação de domínio no substituto: os algoritmos clássicos da WebCrypto (
'Ed25519'e'ECDSA') não leem uma propriedadecontextdo dicionário de algoritmos (o WebIDL ignora silenciosamente as propriedades de dicionário desconhecidas emAlgorithmeEcdsaParams). Adicione um cabeçalhocontextcom prefixo de comprimento ao payload no caminho substituto para que a separação de domínio se comporte de maneira consistente em ambos os pacotes.
const ML_DSA_PUBLIC_KEY_ALGORITHMS = {
1312: 'ML-DSA-44',
1952: 'ML-DSA-65',
2592: 'ML-DSA-87',
};
export function supportsPostQuantumSignatures() {
return (
typeof globalThis.SubtleCrypto?.supports === 'function' &&
SubtleCrypto.supports('sign', 'ML-DSA-65')
);
}
export async function generateSigningKeyPair() {
if (supportsPostQuantumSignatures()) {
const keyPair = await crypto.subtle.generateKey('ML-DSA-65', false, ['sign', 'verify']);
const publicKeyBytes = new Uint8Array(
await crypto.subtle.exportKey('raw-public', keyPair.publicKey),
);
return {
suite: 'ML-DSA-65',
keyFormat: 'raw-public',
keyPair,
publicKeyBytes,
};
}
// Classical fallback: Ed25519
const keyPair = await crypto.subtle.generateKey('Ed25519', false, ['sign', 'verify']);
const publicKeyBytes = new Uint8Array(await crypto.subtle.exportKey('raw', keyPair.publicKey));
return {
suite: 'Ed25519',
keyFormat: 'raw',
keyPair,
publicKeyBytes,
};
}
function frameFallbackInput(payloadBytes, contextBytes) {
if (contextBytes.byteLength > 255) {
throw new DOMException('Context must not exceed 255 bytes', 'OperationError');
}
const framed = new Uint8Array(1 + contextBytes.byteLength + payloadBytes.byteLength);
framed[0] = contextBytes.byteLength;
framed.set(contextBytes, 1);
framed.set(payloadBytes, 1 + contextBytes.byteLength);
return framed;
}
export async function signPayload(
privateKey,
payloadBytes,
contextBytes = new Uint8Array(0),
) {
if (privateKey.algorithm.name.startsWith('ML-DSA')) {
const sig = await crypto.subtle.sign(
{ name: privateKey.algorithm.name, context: contextBytes },
privateKey,
payloadBytes,
);
return new Uint8Array(sig);
}
const framed = frameFallbackInput(payloadBytes, contextBytes);
const sig = await crypto.subtle.sign('Ed25519', privateKey, framed);
return new Uint8Array(sig);
}
export async function verifyPayload(
publicKeyOrBytes,
signatureBytes,
payloadBytes,
contextBytes = new Uint8Array(0),
) {
const publicKey =
publicKeyOrBytes instanceof CryptoKey
? publicKeyOrBytes
: await (() => {
const bytes =
publicKeyOrBytes instanceof Uint8Array
? publicKeyOrBytes
: new Uint8Array(publicKeyOrBytes);
const mlDsaName = ML_DSA_PUBLIC_KEY_ALGORITHMS[bytes.byteLength];
return mlDsaName
? crypto.subtle.importKey('raw-public', bytes, mlDsaName, true, ['verify'])
: crypto.subtle.importKey('raw', bytes, 'Ed25519', true, ['verify']);
})();
if (publicKey.algorithm.name.startsWith('ML-DSA')) {
return crypto.subtle.verify(
{ name: publicKey.algorithm.name, context: contextBytes },
publicKey,
signatureBytes,
payloadBytes,
);
}
const framed = frameFallbackInput(payloadBytes, contextBytes);
return crypto.subtle.verify('Ed25519', publicKey, signatureBytes, framed);
}