Utilisez l'API Web Cryptography (crypto.subtle) pour signer et valider des messages, des jetons ou des artefacts dans le navigateur à l'aide de clés asymétriques résistantes à l'informatique quantique (ML-DSA) ou classiques ('Ed25519', 'ECDSA'). Les signatures numériques garantissent l'intégrité des données et l'authenticité du signataire sans nécessiter de clés secrètes partagées. Pour savoir comment établir des secrets partagés et chiffrer des charges utiles confidentielles, consultez Chiffrer des données côté client avec WebCrypto.
1. Choisir un algorithme et générer une paire de clés
ML-DSA (Module-Lattice-Based Digital Signature Algorithm, normalisé dans NIST FIPS 204) fournit des signatures numériques post-quantiques basées sur des réseaux, résistantes aux attaques des ordinateurs classiques et quantiques.
Utilisez 'ML-DSA-65' par défaut pour la signature à usage général. Choisissez 'ML-DSA-44' lorsque vous avez besoin de clés publiques et de signatures plus petites sur des canaux à bande passante limitée et qu'une sécurité de 128 bits est suffisante, ou 'ML-DSA-87' lorsqu'une règle de sécurité nécessite la conformité à la catégorie 5 du NIST (CNSA 2.0).
| Nom de l'algorithme | Niveau de sécurité et cas d'utilisation | Clé publique ('raw-public') |
Taille de la signature |
|---|---|---|---|
'ML-DSA-44' |
NIST de catégorie 2 (sécurité d'environ 128 bits) ; clés et signatures les plus petites | 1 312 octets | 2 420 octets |
'ML-DSA-65' |
Catégorie 3 du NIST (sécurité d'environ 192 bits) ; valeur par défaut recommandée | 1952 octets | 3309 octets |
'ML-DSA-87' |
NIST de catégorie 5 (sécurité d'environ 256 bits) ; marge de sécurité la plus élevée | 2 592 octets | 4 627 octets |
- Transmettez
'ML-DSA-44','ML-DSA-65'ou'ML-DSA-87'comme nom d'algorithme (les noms antérieurs à la standardisation, tels que'Dilithium'ou'CRYSTALS-Dilithium', génèrent une erreurNotSupportedError). - Ni ML-DSA, ni
'Ed25519'n'acceptent de paramètrehash. - Transmettez
['sign', 'verify']lors de la génération d'une paire de clés avecgenerateKey():
const signerKeyPair = await crypto.subtle.generateKey(
'ML-DSA-65',
false,
['sign', 'verify'],
);
2. Exporter et importer des clés
'ML-DSA-44', 'ML-DSA-65' et 'ML-DSA-87' rejettent l'ancien format 'raw' dans importKey() et exportKey() avec un NotSupportedError. Exportez ou importez toujours les clés à l'aide d'un format explicite :
'raw-public': octets de la clé publique (octets1312,1952ou2592). Transmettez['verify']comme utilisation de la clé lors de l'importation d'une clé publique.'raw-seed': valeur source de clé privée de 32 octets pour les trois algorithmes ML-DSA.'raw'et'raw-private'génèrentNotSupportedErrorsur ML-DSA. Stockez ou transmettez les clés privées à l'aide de'raw-seed','pkcs8'(54 octets) ou'jwk'(kty: 'AKP'). Transmettez['sign']en tant qu'utilisation de la clé lors de l'importation d'une clé privée (la transmission de['sign', 'verify']àimportKey()génèreSyntaxError).'spki','pkcs8'et'jwk': compatibles avec les trois algorithmes ML-DSA.
Exportez toujours la clé publique de validation avec 'raw-public' afin que les octets de la clé publique puissent être sérialisés ou partagés avec les validateurs :
// 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'],
);
Si vous ne détenez qu'une CryptoKey de signature privée (même avec extractable: false) et que vous avez besoin de la CryptoKey publique correspondante, appelez crypto.subtle.getPublicKey() avec ['verify'] :
const publicKey = await crypto.subtle.getPublicKey(
signerKeyPair.privateKey,
['verify'],
);
3. Signer et vérifier des charges utiles
Appelez crypto.subtle.sign() avec la clé privée pour produire une signature, et crypto.subtle.verify() avec la clé publique pour la vérifier :
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,
);
Séparation des domaines avec ContextParams
Pour éviter qu'une signature créée dans un but précis ne soit réutilisée dans une autre partie de votre application ou de votre protocole qui partage la même paire de clés, associez la signature à un context de séparation de domaine.
Au lieu d'une simple chaîne de nom d'algorithme, transmettez un dictionnaire ContextParams ({ name, context }) à sign() et verify(), où context est un 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 octets :
contextaccepte de0à255octets (omettrecontextéquivaut à transmettre unnew Uint8Array(0)vide). Si vous transmettez plus de255octets, la requête est rejetée avecOperationError. - Correspondance exacte requise :
crypto.subtle.verify()ne renvoietrueque lorsque le vérificateur transmet exactement les mêmes octetscontextque ceux utilisés lors decrypto.subtle.sign().
Stratégies de remplacement
Si votre cible Baseline n'est pas encore compatible avec SubtleCrypto.supports() ou ML-DSA, détectez la compatibilité à l'aide de la méthode SubtleCrypto.supports() statique et revenez à la méthode 'Ed25519' classique (ou 'ECDSA' avec 'P-256' et 'SHA-256' lorsque vous ciblez des navigateurs antérieurs à la compatibilité avec 'Ed25519') :
- Appelez
SubtleCrypto.supports()sur le constructeurSubtleCrypto, et non surcrypto.subtle:globalThis.SubtleCrypto?.supports?.('sign', 'ML-DSA-65')est une méthode statique synchrone qui renvoie une valeur booléenne (crypto.subtle.supportsestundefined). Sondez'sign','verify','generateKey','importKey','exportKey'ou'getPublicKey'(ou transmettez un dictionnaireContextParamstel queSubtleCrypto.supports('sign', { name: 'ML-DSA-65', context })). - Format de clé dans la solution de secours (
'raw') : bien que les navigateurs compatibles avec Modern WebCrypto acceptent également'raw-public'sur'Ed25519'et'ECDSA', utilisez'raw'dans les chemins de secours ciblant les anciens navigateurs. - Séparation de domaine dans le remplacement : les algorithmes WebCrypto classiques (
'Ed25519'et'ECDSA') ne lisent pas de propriétécontextà partir du dictionnaire d'algorithmes (WebIDL ignore silencieusement les propriétés de dictionnaire inconnues surAlgorithmetEcdsaParams). Ajoutez un en-têtecontextpréfixé par la longueur à la charge utile dans votre chemin de remplacement afin que la séparation de domaine se comporte de manière cohérente dans les deux suites.
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);
}