Gebruik de Web Cryptography API (crypto.subtle) om berichten, tokens of artefacten in de browser te ondertekenen en te verifiëren met quantumresistente (ML-DSA) of klassieke ('Ed25519', 'ECDSA') asymmetrische sleutels. Digitale handtekeningen garanderen de gegevensintegriteit en de authenticiteit van de ondertekenaar zonder dat er gedeelde geheime sleutels nodig zijn. Ga naar Gegevens aan de clientzijde versleutelen met WebCrypto voor meer informatie over hoe je gedeelde geheimen instelt en vertrouwelijke payloads versleutelt.
1. Kies een algoritme en genereer een sleutelpaar
ML-DSA (Module-Lattice-Based Digital Signature Algorithm, gestandaardiseerd in NIST FIPS 204) biedt post-quantum digitale handtekeningen op basis van roosters die bestand zijn tegen aanvallen door zowel klassieke als quantumcomputers.
Gebruik standaard 'ML-DSA-65' voor algemene ondertekening. Kies 'ML-DSA-44' als je kleinere openbare sleutels en handtekeningen nodig hebt via kanalen met beperkte bandbreedte en 128-bits beveiliging voldoende is, of 'ML-DSA-87' als een beveiligingsbeleid naleving van NIST-categorie 5 (CNSA 2.0) vereist.
| Algoritmenaam | Beveiligingsniveau en use case | Openbare sleutel ('raw-public') |
Grootte van handtekening |
|---|---|---|---|
'ML-DSA-44' |
NIST-categorie 2 (~128-bits beveiliging); kleinste sleutels en handtekeningen | 1312 bytes | 2420 bytes |
'ML-DSA-65' |
NIST-categorie 3 (~192-bits beveiliging); aanbevolen standaard | 1952 bytes | 3309 bytes |
'ML-DSA-87' |
NIST-categorie 5 (~256-bits beveiliging); hoogste beveiligingsmarge | 2592 bytes | 4627 bytes |
- Geef
'ML-DSA-44','ML-DSA-65'of'ML-DSA-87'door als de algoritmenaam (namen van vóór de standaardisatie, zoals'Dilithium'of'CRYSTALS-Dilithium', genererenNotSupportedError). - Noch ML-DSA, noch
'Ed25519'neemt eenhash-parameter. - Geef
['sign', 'verify']door als je een sleutelpaar genereert metgenerateKey():
const signerKeyPair = await crypto.subtle.generateKey(
'ML-DSA-65',
false,
['sign', 'verify'],
);
2. Sleutels exporteren en importeren
'ML-DSA-44', 'ML-DSA-65' en 'ML-DSA-87' wijzen de verouderde 'raw'-indeling af in importKey() en exportKey() met een NotSupportedError. Exporteer of importeer sleutels altijd met een expliciete indeling:
'raw-public': Bytes van openbare sleutel (1312,1952of2592bytes). Geef['verify']door als sleutelgebruik als je een openbare sleutel importeert.'raw-seed': Privésleutel-seed van 32 bytes voor alle 3 de ML-DSA-algoritmen. Zowel'raw'als'raw-private'genererenNotSupportedErrorop ML-DSA. Sla privésleutels op of verzend ze met'raw-seed','pkcs8'(54 bytes) of'jwk'(kty: 'AKP'). Geef['sign']door als sleutelgebruik bij het importeren van een privésleutel (als je['sign', 'verify']doorgeeft aanimportKey(), wordtSyntaxErrorgegenereerd).'spki','pkcs8'en'jwk': Ondersteund voor alle 3 de ML-DSA-algoritmen.
Exporteer de openbare verificatiesleutel altijd met 'raw-public', zodat de bytes van de openbare sleutel kunnen worden geserialiseerd of gedeeld met verificateurs:
// 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'],
);
Als je alleen een privésleutel voor ondertekening CryptoKey hebt (zelfs met extractable: false) en de bijbehorende openbare sleutel CryptoKey nodig hebt, roep je crypto.subtle.getPublicKey() aan met ['verify']:
const publicKey = await crypto.subtle.getPublicKey(
signerKeyPair.privateKey,
['verify'],
);
3. Payloads ondertekenen en verifiëren
Roep crypto.subtle.sign() aan met de privésleutel om een handtekening te maken en crypto.subtle.verify() met de openbare sleutel om deze te controleren:
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,
);
Domeinscheiding met ContextParams
Als je wilt voorkomen dat een handtekening die voor een bepaald doel is gemaakt, opnieuw wordt afgespeeld in een ander deel van je app of protocol dat hetzelfde sleutelpaar deelt, bind je de handtekening aan een domeinscheiding context.
In plaats van een tekenreeks met een kale algoritmenaam geef je een ContextParams-woordenboek ({ name, context }) door aan sign() en verify(), waarbij context een BufferSource is (Uint8Array of 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,
);
- Limiet van 255 bytes:
contextaccepteert0tot255bytes (als jecontextweglaat, is dat hetzelfde als een legenew Uint8Array(0)doorgeven). Als je meer dan255bytes doorgeeft, wordt dit geweigerd metOperationError. - Exacte overeenkomst vereist:
crypto.subtle.verify()retourneerttruealleen als de verificateur exact dezelfdecontextbytes doorgeeft die zijn gebruikt tijdenscrypto.subtle.sign().
Reservestrategieën
Als je Baseline-doel SubtleCrypto.supports() of ML-DSA nog niet ondersteunt, spoor je ondersteuning op met de statische SubtleCrypto.supports()-methode en val je terug op klassieke 'Ed25519' (of 'ECDSA' met 'P-256' en 'SHA-256' als je browsers target van vóór 'Ed25519'-ondersteuning):
- Roep
SubtleCrypto.supports()aan op deSubtleCrypto-constructor, niet opcrypto.subtle:globalThis.SubtleCrypto?.supports?.('sign', 'ML-DSA-65')is een synchrone statische methode die een Booleaanse waarde retourneert (crypto.subtle.supportsisundefined). Test'sign','verify','generateKey','importKey','exportKey'of'getPublicKey'(of geef eenContextParams-woordenboek door, zoalsSubtleCrypto.supports('sign', { name: 'ML-DSA-65', context })). - Sleutelindeling in de fallback (
'raw'): Hoewel browsers die Modern WebCrypto ondersteunen ook'raw-public'accepteren op'Ed25519'en'ECDSA', gebruik je'raw'in fallback-paden die oudere browsers targeten. - Domeinscheiding in de fallback: Klassieke WebCrypto-algoritmen (
'Ed25519'en'ECDSA') lezen geencontext-property uit het algoritmedictioary (WebIDL negeert onbekende dictionary-property's opAlgorithmenEcdsaParams). Voeg eencontext-header met lengtevoorvoegsel toe aan de payload in je fallback-pad, zodat domeinscheiding consistent werkt in beide 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);
}