Mit der Web Cryptography API (crypto.subtle) können Sie Nachrichten, Tokens oder Artefakte im Browser mit quantenresistenten (ML-DSA) oder klassischen ('Ed25519', 'ECDSA') asymmetrischen Schlüsseln signieren und verifizieren. Digitale Signaturen garantieren die Datenintegrität und die Authentizität des Signierenden, ohne dass gemeinsame geheime Schlüssel erforderlich sind. Informationen zum Einrichten von gemeinsamen Secrets und zum Verschlüsseln vertraulicher Nutzlasten finden Sie unter Daten clientseitig mit WebCrypto verschlüsseln.
1. Algorithmus auswählen und Schlüsselpaar generieren
ML-DSA (Module-Lattice-Based Digital Signature Algorithm, standardisiert in NIST FIPS 204) bietet Post-Quanten-gitterbasierte digitale Signaturen, die Angriffen sowohl von klassischen als auch von Quantencomputern standhalten.
Verwenden Sie standardmäßig 'ML-DSA-65' für allgemeine Zwecke. Wählen Sie 'ML-DSA-44', wenn Sie kleinere öffentliche Schlüssel und Signaturen über bandbreitenbeschränkte Kanäle benötigen und eine 128-Bit-Sicherheit ausreicht, oder 'ML-DSA-87', wenn eine Sicherheitsrichtlinie die Einhaltung von NIST-Kategorie 5 (CNSA 2.0) erfordert.
| Algorithmusname | Sicherheitsniveau und Anwendungsfall | Öffentlicher Schlüssel ('raw-public') |
Signaturgröße |
|---|---|---|---|
'ML-DSA-44' |
NIST-Kategorie 2 (~128-Bit-Sicherheit); kleinste Schlüssel und Signaturen | 1.312 Byte | 2.420 Byte |
'ML-DSA-65' |
NIST-Kategorie 3 (~192-Bit-Sicherheit); empfohlener Standardwert | 1.952 Byte | 3.309 Byte |
'ML-DSA-87' |
NIST-Kategorie 5 (~256-Bit-Sicherheit); höchste Sicherheitsmarge | 2.592 Byte | 4.627 Byte |
- Übergeben Sie
'ML-DSA-44','ML-DSA-65'oder'ML-DSA-87'als Algorithmusnamen. Bei Namen vor der Standardisierung wie'Dilithium'oder'CRYSTALS-Dilithium'wirdNotSupportedErrorausgegeben. - Weder ML-DSA noch
'Ed25519'akzeptieren einenhash-Parameter. - Übergeben Sie
['sign', 'verify'], wenn Sie ein Schlüsselpaar mitgenerateKey()generieren:
const signerKeyPair = await crypto.subtle.generateKey(
'ML-DSA-65',
false,
['sign', 'verify'],
);
2. Schlüssel exportieren und importieren
'ML-DSA-44', 'ML-DSA-65' und 'ML-DSA-87' lehnen das alte 'raw'-Format in importKey() und exportKey() mit einem NotSupportedError ab. Exportieren oder importieren Sie Schlüssel immer mit einem expliziten Format:
'raw-public': Byte des öffentlichen Schlüssels (1312-,1952- oder2592-Byte). Übergeben Sie['verify']als Schlüsselverwendung, wenn Sie einen öffentlichen Schlüssel importieren.'raw-seed': 32-Byte-Seed für den privaten Schlüssel für alle drei ML-DSA-Algorithmen. Sowohl'raw'als auch'raw-private'geben bei ML-DSANotSupportedErroraus. Speichern oder übertragen Sie private Schlüssel mit'raw-seed','pkcs8'(54 Byte) oder'jwk'(kty: 'AKP'). Übergeben Sie['sign']als Schlüsselverwendung, wenn Sie einen privaten Schlüssel importieren. Wenn Sie['sign', 'verify']animportKey()übergeben, wirdSyntaxErrorausgegeben.'spki','pkcs8'und'jwk': Unterstützt für alle drei ML-DSA-Algorithmen.
Exportieren Sie den öffentlichen Schlüssel für die Bestätigung immer mit 'raw-public', damit die Byte des öffentlichen Schlüssels serialisiert oder mit Prüfern geteilt werden können:
// 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'],
);
Wenn Sie nur einen privaten Signatur-CryptoKey haben (auch mit extractable: false) und den entsprechenden öffentlichen CryptoKey benötigen, rufen Sie crypto.subtle.getPublicKey() mit ['verify'] auf:
const publicKey = await crypto.subtle.getPublicKey(
signerKeyPair.privateKey,
['verify'],
);
3. Nutzlasten signieren und verifizieren
Rufen Sie crypto.subtle.sign() mit dem privaten Schlüssel auf, um eine Signatur zu erstellen, und crypto.subtle.verify() mit dem öffentlichen Schlüssel, um sie zu prüfen:
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,
);
Domänen-Trennung mit ContextParams
Um zu verhindern, dass eine für einen bestimmten Zweck erstellte Signatur in einem anderen Teil Ihrer Anwendung oder Ihres Protokolls, der dasselbe Schlüsselpaar verwendet, wiedergegeben wird, binden Sie die Signatur an eine Domain-Separation-context.
Übergeben Sie anstelle eines einfachen Algorithmusnamensstrings ein ContextParams-Dictionary ({ name, context }) an sign() und verify(), wobei context ein BufferSource (Uint8Array oder ArrayBuffer) ist:
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,
);
- Limit von 255 Byte:
contextakzeptiert0bis255Byte. Wenncontextweggelassen wird, entspricht dies der Übergabe eines leerennew Uint8Array(0). Wenn mehr als255Byte übergeben werden, wird die Anfrage mitOperationErrorabgelehnt. - Genaue Übereinstimmung erforderlich:
crypto.subtle.verify()gibttruenur zurück, wenn der Prüfer genau dieselbencontext-Bytes übergibt, die währendcrypto.subtle.sign()verwendet wurden.
Fallback-Strategien
Wenn Ihr Baseline-Ziel SubtleCrypto.supports() oder ML-DSA noch nicht unterstützt, können Sie die Unterstützung mit der statischen SubtleCrypto.supports()-Methode erkennen und auf das klassische 'Ed25519' (oder 'ECDSA' mit 'P-256' und 'SHA-256', wenn Sie Browser vor der 'Ed25519'-Unterstützung verwenden) zurückgreifen:
- Rufen Sie
SubtleCrypto.supports()imSubtleCrypto-Konstruktor auf, nicht incrypto.subtle:globalThis.SubtleCrypto?.supports?.('sign', 'ML-DSA-65')ist eine synchrone statische Methode, die einen booleschen Wert zurückgibt (crypto.subtle.supportsistundefined). Prüfen Sie'sign','verify','generateKey','importKey','exportKey'oder'getPublicKey'(oder übergeben Sie einContextParams-Wörterbuch wieSubtleCrypto.supports('sign', { name: 'ML-DSA-65', context })). - Schlüsselformat im Fallback (
'raw'): Browser, die Modern WebCrypto unterstützen, akzeptieren zwar auch'raw-public'für'Ed25519'und'ECDSA', aber in Fallback-Pfaden für ältere Browser sollten Sie'raw'verwenden. - Domain-Trennung im Fallback: Klassische WebCrypto-Algorithmen (
'Ed25519'und'ECDSA') lesen keinecontext-Eigenschaft aus dem Algorithmus-Dictionary (WebIDL ignoriert unbekannte Dictionary-Eigenschaften beiAlgorithmundEcdsaParamsstillschweigend). Stellen Sie der Nutzlast im Fallback-Pfad einencontext-Header mit Längenpräfix voran, damit die Domain-Trennung in beiden Suites einheitlich funktioniert.
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);
}