Utilizza l'API Web Cryptography (crypto.subtle) per firmare e verificare messaggi, token o artefatti nel browser utilizzando chiavi asimmetriche resistenti ai computer quantistici (ML-DSA) o classiche ('Ed25519', 'ECDSA'). Le firme digitali garantiscono l'integrità dei dati e l'autenticità del firmatario senza richiedere chiavi segrete condivise. Per stabilire secret condivisi e criptare payload riservati, consulta Cripta i dati lato client con WebCrypto.
1. Scegli un algoritmo e genera una coppia di chiavi
ML-DSA (Module-Lattice-Based Digital Signature Algorithm, standardizzato in NIST FIPS 204) fornisce firme digitali basate su reticolo post-quantistiche resistenti agli attacchi di computer classici e quantistici.
Utilizza 'ML-DSA-65' per impostazione predefinita per la firma per uso generico. Scegli 'ML-DSA-44' quando hai bisogno di chiavi pubbliche e firme più piccole su canali con larghezza di banda limitata e la sicurezza a 128 bit è sufficiente, oppure 'ML-DSA-87' quando una policy di sicurezza richiede la conformità con la categoria 5 del NIST (CNSA 2.0).
| Nome algoritmo | Livello di sicurezza e caso d'uso | Chiave pubblica ('raw-public') |
Dimensioni della firma |
|---|---|---|---|
'ML-DSA-44' |
NIST Categoria 2 (sicurezza ~128 bit); chiavi e firme più piccole | 1312 byte | 2420 byte |
'ML-DSA-65' |
Categoria 3 NIST (sicurezza ~192 bit); impostazione predefinita consigliata | 1952 byte | 3309 byte |
'ML-DSA-87' |
NIST Categoria 5 (sicurezza ~256 bit); margine di sicurezza più elevato | 2592 byte | 4627 byte |
- Passa
'ML-DSA-44','ML-DSA-65'o'ML-DSA-87'come nome dell'algoritmo (i nomi pre-standardizzazione come'Dilithium'o'CRYSTALS-Dilithium'generanoNotSupportedError). - Né ML-DSA né
'Ed25519'accettano un parametrohash. - Passa
['sign', 'verify']quando generi una coppia di chiavi congenerateKey():
const signerKeyPair = await crypto.subtle.generateKey(
'ML-DSA-65',
false,
['sign', 'verify'],
);
2. Esporta e importa le chiavi
'ML-DSA-44', 'ML-DSA-65' e 'ML-DSA-87' rifiutano il formato 'raw' legacy in importKey() e exportKey() con un NotSupportedError. Esporta o importa sempre le chiavi utilizzando un formato esplicito:
'raw-public': byte della chiave pubblica (1312,1952o2592byte). Passa['verify']come utilizzo della chiave durante l'importazione di una chiave pubblica.'raw-seed': seed della chiave privata da 32 byte per tutti e tre gli algoritmi ML-DSA. Sia'raw'che'raw-private'generanoNotSupportedErrorsu ML-DSA: archivia o trasmetti le chiavi private utilizzando'raw-seed','pkcs8'(54 byte) o'jwk'(kty: 'AKP'). Passa['sign']come utilizzo della chiave durante l'importazione di una chiave privata (il passaggio di['sign', 'verify']aimportKey()generaSyntaxError).'spki','pkcs8'e'jwk': supportati per tutti e tre gli algoritmi ML-DSA.
Esporta sempre la chiave pubblica di verifica con 'raw-public' in modo che i byte della chiave pubblica possano essere serializzati o condivisi con i verificatori:
// 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 hai solo una CryptoKey di firma privata (anche con extractable: false) e hai bisogno della CryptoKey pubblica corrispondente, chiama crypto.subtle.getPublicKey() con ['verify']:
const publicKey = await crypto.subtle.getPublicKey(
signerKeyPair.privateKey,
['verify'],
);
3. Firma e verifica i payload
Chiama crypto.subtle.sign() con la chiave privata per generare una firma e crypto.subtle.verify() con la chiave pubblica per verificarla:
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,
);
Separazione dei domini con ContextParams
Per evitare che una firma creata per uno scopo venga riprodotta in un'altra parte dell'applicazione o del protocollo che condivide la stessa coppia di chiavi, associa la firma a un context di separazione dei domini.
Anziché una stringa con il nome dell'algoritmo, passa un dizionario ContextParams ({ name, context }) a sign() e verify(), dove context è un BufferSource (Uint8Array o 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 di 255 byte:
contextaccetta da0a255byte (ometterecontextequivale a passare unnew Uint8Array(0)vuoto). Se passi più di255byte, l'operazione viene rifiutata conOperationError. - Corrispondenza esatta richiesta:
crypto.subtle.verify()restituiscetruesolo quando il verificatore passa esattamente gli stessi bytecontextutilizzati durantecrypto.subtle.sign().
Strategie di riserva
Se il tuo target Baseline non supporta ancora SubtleCrypto.supports() o ML-DSA, rileva il supporto utilizzando il metodo statico SubtleCrypto.supports() ed esegui il fallback al classico 'Ed25519' (o 'ECDSA' con 'P-256' e 'SHA-256' quando scegli come target i browser precedenti al supporto di 'Ed25519'):
- Chiama
SubtleCrypto.supports()sul costruttoreSubtleCrypto, non sucrypto.subtle:globalThis.SubtleCrypto?.supports?.('sign', 'ML-DSA-65')è un metodo statico sincrono che restituisce un valore booleano (crypto.subtle.supportsèundefined). Esegui il probe di'sign','verify','generateKey','importKey','exportKey'o'getPublicKey'(o passa un dizionarioContextParamscomeSubtleCrypto.supports('sign', { name: 'ML-DSA-65', context })). - Formato della chiave nel fallback (
'raw'): anche se i browser che supportano Modern WebCrypto accettano anche'raw-public'su'Ed25519'e'ECDSA', utilizza'raw'nei percorsi di fallback che hanno come target i browser meno recenti. - Separazione dei domini nel fallback: gli algoritmi WebCrypto classici (
'Ed25519'e'ECDSA') non leggono una proprietàcontextdal dizionario degli algoritmi (WebIDL ignora silenziosamente le proprietà del dizionario sconosciute suAlgorithmeEcdsaParams). Anteponi un'intestazionecontextcon prefisso di lunghezza al payload nel percorso di fallback in modo che la separazione dei domini si comporti in modo coerente in entrambe le suite.
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);
}