Криптографическая подпись в Web Crypto API строится вокруг интерфейса
SubtleCrypto, доступного через crypto.subtle.
Основная идея заключается в том, что данные преобразуются в
фиксированную хэш-сумму, а затем эта сумма подписывается закрытым
ключом. Проверка выполняется с использованием открытого ключа или общего
секретного ключа в зависимости от выбранного алгоритма.
Поддерживаются три основных подхода:
Каждый из этих механизмов решает задачу подтверждения целостности данных и их происхождения, но отличается моделью ключей и криптографической стойкостью.
Любая операция подписи в Web Crypto API сводится к следующей последовательности:
ArrayBuffer)signverifyКлючевой особенностью API является работа исключительно с бинарными данными. Строки и объекты должны быть явно сериализованы.
Пример преобразования строки:
const encoder = new TextEncoder();
const data = encoder.encode("message");
HMAC использует один и тот же секретный ключ для подписи и проверки. Это делает его простым, но ограничивает использование в распределённых системах.
const key = await crypto.subtle.generateKey(
{
name: "HMAC",
hash: "SHA-256"
},
true,
["sign", "verify"]
);
hash определяет алгоритм хэшированияextractable: true позволяет экспортировать ключkeyUsages определяет допустимые операцииconst signature = await crypto.subtle.sign(
"HMAC",
key,
data
);
Результат — ArrayBuffer, содержащий подпись.
const isValid = await crypto.subtle.verify(
"HMAC",
key,
signature,
data
);
Если данные были изменены хотя бы на один байт, проверка вернёт
false.
RSA-PSS (Probabilistic Signature Scheme) считается более безопасной версией классической RSA-подписи за счёт использования случайной соли.
const keyPair = await crypto.subtle.generateKey(
{
name: "RSA-PSS",
modulusLength: 2048,
publicExponent: new Uint8Array([1, 0, 1]),
hash: "SHA-256"
},
true,
["sign", "verify"]
);
modulusLength определяет длину ключа в битахpublicExponent обычно фиксирован как 65537hash задаёт алгоритм хэширования перед подписьюconst signature = await crypto.subtle.sign(
{
name: "RSA-PSS",
saltLength: 32
},
keyPair.privateKey,
data
);
Параметр saltLength влияет на криптографическую
стойкость.
const isValid = await crypto.subtle.verify(
{
name: "RSA-PSS",
saltLength: 32
},
keyPair.publicKey,
signature,
data
);
Важно, чтобы параметры подписи и проверки совпадали, иначе результат будет некорректным.
ECDSA обеспечивает сопоставимую с RSA стойкость при меньшем размере ключей, что делает его предпочтительным в браузерной криптографии.
const keyPair = await crypto.subtle.generateKey(
{
name: "ECDSA",
namedCurve: "P-256"
},
true,
["sign", "verify"]
);
Наиболее распространённые кривые:
P-256 — баланс безопасности и производительностиP-384 — повышенная стойкостьP-521 — максимальная стойкостьconst signature = await crypto.subtle.sign(
{
name: "ECDSA",
hash: "SHA-256"
},
keyPair.privateKey,
data
);
ECDSA требует обязательного указания хэш-функции.
const isValid = await crypto.subtle.verify(
{
name: "ECDSA",
hash: "SHA-256"
},
keyPair.publicKey,
signature,
data
);
Поскольку Web Crypto API работает только с бинарными буферами, сложные структуры необходимо сериализовать.
Типичный подход:
const obj = {
user: "alice",
role: "admin",
timestamp: 1710000000
};
const encoder = new TextEncoder();
const data = encoder.encode(JSON.stringify(obj));
Важно учитывать:
Для стабильности часто используют детерминированную сериализацию.
Во всех асимметричных схемах (RSA-PSS, ECDSA) Web Crypto API выполняет хэширование автоматически, но иногда требуется явное вычисление хэша:
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
Это полезно в сценариях:
Ключи можно сохранять и восстанавливать:
const exported = await crypto.subtle.exportKey(
"pkcs8",
privateKey
);
или
const exportedPublic = await crypto.subtle.exportKey(
"spki",
publicKey
);
const key = await crypto.subtle.importKey(
"pkcs8",
exported,
{
name: "RSA-PSS",
hash: "SHA-256"
},
true,
["sign"]
);
Форматы:
pkcs8 — приватные ключиspki — публичные ключиraw — для симметричных ключей (HMAC)Типичная архитектура выглядит следующим образом:
ArrayBuffer{data, signature}Пример структуры:
{
data: "eyJ1c2VyIjoiYWxpY2UifQ==",
signature: "base64..."
}
Подпись и проверка должны использовать одинаковые параметры:
Любое изменение байтового представления делает подпись недействительной:
signverifyJSON без детерминированного порядка ключей приводит к невозможности проверки подписи.
HMAC
RSA-PSS
ECDSA
Web Crypto API не работает со строками напрямую. Используются:
TextEncoder для преобразования строк →
Uint8ArrayTextDecoder для обратного преобразованияArrayBuffer как основной формат передачи данныхПример:
const encoder = new TextEncoder();
const data = encoder.encode("secure message");
Подписанные данные часто хранятся или передаются в следующих форматах:
Конвертация:
function toBase64(buffer) {
return btoa(String.fromCharCode(...new Uint8Array(buffer)));
}
// Подпись
const signature = await crypto.subtle.sign(
{ name: "ECDSA", hash: "SHA-256" },
privateKey,
data
);
// Проверка
const valid = await crypto.subtle.verify(
{ name: "ECDSA", hash: "SHA-256" },
publicKey,
signature,
data
);
Криптографическая подпись не защищает данные сама по себе, она лишь обеспечивает:
Реальная безопасность зависит от: