Używaj interfejsu Web Cryptography API (crypto.subtle) do podpisywania i weryfikowania wiadomości, tokenów lub artefaktów w przeglądarce za pomocą odpornych na kwanty (ML-DSA) lub klasycznych ('Ed25519', 'ECDSA') kluczy asymetrycznych. Podpisy cyfrowe gwarantują integralność danych i autentyczność podpisującego bez konieczności udostępniania tajnych kluczy. Informacje o ustalaniu wspólnych obiektów tajnych i szyfrowaniu poufnych danych znajdziesz w artykule Szyfrowanie danych po stronie klienta za pomocą WebCrypto.
1. Wybieranie algorytmu i generowanie pary kluczy
ML-DSA (Module-Lattice-Based Digital Signature Algorithm, ustandaryzowany w NIST FIPS 204) zapewnia postkwantowe podpisy cyfrowe oparte na sieci, które są odporne na ataki zarówno ze strony komputerów klasycznych, jak i kwantowych.
Domyślnie używaj 'ML-DSA-65' do podpisywania do zwykłych obciążeń. Wybierz 'ML-DSA-44', gdy potrzebujesz mniejszych kluczy publicznych i podpisów w kanałach o ograniczonej przepustowości, a 128-bitowe zabezpieczenia są wystarczające, lub 'ML-DSA-87', gdy wymagania wynikające z zasad bezpieczeństwa wymagają zgodności z kategorią 5 NIST (CNSA 2.0).
| Nazwa algorytmu | Poziom bezpieczeństwa i przypadek użycia | Klucz publiczny ('raw-public') |
Rozmiar podpisu |
|---|---|---|---|
'ML-DSA-44' |
Kategoria 2 NIST (zabezpieczenia ~128-bitowe); najmniejsze klucze i podpisy | 1312 bajtów | 2420 bajtów |
'ML-DSA-65' |
Kategoria 3 NIST (zabezpieczenia ~192-bitowe); zalecane ustawienie domyślne | 1952 bajty | 3309 bajtów |
'ML-DSA-87' |
NIST Category 5 (zabezpieczenia ~256-bitowe); najwyższy margines bezpieczeństwa | 2592 bajty | 4627 bajtów |
- Przekaż
'ML-DSA-44','ML-DSA-65'lub'ML-DSA-87'jako nazwę algorytmu (nazwy sprzed standaryzacji, takie jak'Dilithium'lub'CRYSTALS-Dilithium', powodują błądNotSupportedError). - Ani ML-DSA, ani
'Ed25519'nie przyjmują parametruhash. - Podczas generowania pary kluczy za pomocą
generateKey()podaj['sign', 'verify']:
const signerKeyPair = await crypto.subtle.generateKey(
'ML-DSA-65',
false,
['sign', 'verify'],
);
2. Eksportowanie i importowanie kluczy
'ML-DSA-44', 'ML-DSA-65' i 'ML-DSA-87' odrzucają starszy format 'raw' w importKey() i exportKey() z NotSupportedError. Zawsze eksportuj lub importuj klucze, używając jawnego formatu:
'raw-public': bajty klucza publicznego (1312,1952lub2592bajtów). Podczas importowania klucza publicznego przekaż['verify']jako użycie klucza.'raw-seed': 32-bajtowy klucz prywatny dla wszystkich 3 algorytmów ML-DSA. Zarówno'raw', jak i'raw-private'zgłaszają błądNotSupportedErrorw przypadku ML-DSA – przechowuj lub przesyłaj klucze prywatne za pomocą'raw-seed','pkcs8'(54 bajty) lub'jwk'(kty: 'AKP'). Podczas importowania klucza prywatnego przekaż['sign']jako użycie klucza (przekazanie['sign', 'verify']doimportKey()zgłasza błądSyntaxError).'spki','pkcs8'i'jwk': obsługiwane we wszystkich 3 algorytmach ML-DSA.
Zawsze eksportuj klucz publiczny weryfikacji za pomocą 'raw-public', aby bajty klucza publicznego mogły zostać zserializowane lub udostępnione weryfikatorom:
// 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'],
);
Jeśli masz tylko prywatny klucz podpisywania CryptoKey (nawet z extractable: false) i potrzebujesz odpowiadającego mu publicznego klucza CryptoKey, wywołaj crypto.subtle.getPublicKey() z ['verify']:
const publicKey = await crypto.subtle.getPublicKey(
signerKeyPair.privateKey,
['verify'],
);
3. Podpisywanie i weryfikowanie ładunków
Wywołaj crypto.subtle.sign() z kluczem prywatnym, aby wygenerować podpis, i crypto.subtle.verify() z kluczem publicznym, aby go sprawdzić:
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,
);
Rozdzielanie domen za pomocą ContextParams
Aby zapobiec ponownemu odtworzeniu podpisu utworzonego w jednym celu w innej części aplikacji lub protokołu, która korzysta z tej samej pary kluczy, powiąż podpis z context separacji domen.
Zamiast zwykłego ciągu znaków z nazwą algorytmu przekaż słownik ContextParams ({ name, context }) do sign() i verify(), gdzie context to BufferSource (Uint8Array lub 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,
);
- Limit 255 bajtów:
contextakceptuje od0do255bajtów (pominięciecontextjest równoznaczne z przekazaniem pustegonew Uint8Array(0)). Przekazanie więcej niż255bajtów powoduje odrzucenie zOperationError. - Wymagane dopasowanie ścisłe:
crypto.subtle.verify()zwracatruetylko wtedy, gdy weryfikator przekazuje dokładnie te same bajtycontextużyte podczascrypto.subtle.sign().
Strategie zastępcze
Jeśli Twój cel Baseline nie obsługuje jeszcze SubtleCrypto.supports() ani ML-DSA, wykryj obsługę za pomocą statycznej metody SubtleCrypto.supports() i wróć do klasycznego 'Ed25519' (lub 'ECDSA' z 'P-256' i 'SHA-256', gdy kierujesz reklamy na przeglądarki przed obsługą 'Ed25519'):
- Wywołaj
SubtleCrypto.supports()w konstruktorzeSubtleCrypto, a nie wcrypto.subtle:globalThis.SubtleCrypto?.supports?.('sign', 'ML-DSA-65')to synchroniczna metoda statyczna zwracająca wartość logiczną (crypto.subtle.supportstoundefined). Wywołaj'sign','verify','generateKey','importKey','exportKey'lub'getPublicKey'(albo przekaż słownikContextParams, np.SubtleCrypto.supports('sign', { name: 'ML-DSA-65', context })). - Format klucza w przypadku zastępczego (
'raw'): przeglądarki obsługujące Modern WebCrypto akceptują też'raw-public'w przypadku'Ed25519'i'ECDSA', ale w ścieżkach zastępczych kierowanych na starsze przeglądarki używaj'raw'. - Separacja domen w przypadku zastępczym: klasyczne algorytmy WebCrypto (
'Ed25519'i'ECDSA') nie odczytują właściwościcontextze słownika algorytmów (WebIDL ignoruje nieznane właściwości słownika wAlgorithmiEcdsaParams). Dodaj nagłówekcontextz prefiksem długości do ładunku w ścieżce zastępczej, aby separacja domen działała spójnie w obu pakietach.
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);
}