Подпись токенов и документов на практике

Цифровая подпись используется для подтверждения подлинности данных и их неизменности. В контексте веб-приложений это особенно важно для токенов аутентификации, API-запросов и документов, передаваемых между клиентом и сервером. Библиотека SJCL (Stanford Javascript Crypto Library) предоставляет инструменты для реализации подписи на стороне JavaScript с использованием проверенных криптографических алгоритмов.

Основная задача подписи — гарантировать:

  • Целостность: данные не были изменены после подписания
  • Аутентичность: подпись создана владельцем закрытого ключа
  • Невозможность отказа: отправитель не может отрицать факт подписи

Архитектура подписи в SJCL

SJCL не предоставляет высокоуровневого API для «подписания токенов» как готовой функции, но включает базовые криптографические примитивы:

  • Хэш-функции (SHA-256)
  • Эллиптические кривые (ECC)
  • ECDSA (Elliptic Curve Digital Signature Algorithm)

Подпись строится на комбинации:

  1. Хэширования данных
  2. Подписания хэша приватным ключом

Генерация ключевой пары

Для работы с подписями используется асимметричная криптография. Генерация ключей выполняется следующим образом:

const keys = sjcl.ecc.ecdsa.generateKeys(256);

const privateKey = keys.sec;
const publicKey = keys.pub;
  • privateKey — используется для создания подписи
  • publicKey — используется для проверки подписи

Ключи основаны на эллиптической кривой (обычно c256).


Подписание данных

Перед подписанием данные необходимо привести к хэшированному виду:

const message = "important data";
const hash = sjcl.hash.sha256.hash(message);

Затем выполняется подпись:

const signature = privateKey.sign(hash);

Результат — массив чисел (битовое представление подписи), который обычно кодируется:

const signatureBase64 = sjcl.codec.base64.fromBits(signature);

Проверка подписи

Проверка выполняется с использованием публичного ключа:

const isValid = publicKey.verify(hash, signature);
  • true — подпись корректна
  • false — подпись недействительна

Важно: проверка должна выполняться на стороне, которой доверяют (обычно сервер).


Подпись JWT-подобных токенов

SJCL можно использовать для создания собственных токенов, аналогичных JWT:

Формирование токена

const header = { alg: "ES256", typ: "JWT" };
const payload = { userId: 123, exp: Date.now() + 3600000 };

const encodedHeader = btoa(JSON.stringify(header));
const encodedPayload = btoa(JSON.stringify(payload));

const data = encodedHeader + "." + encodedPayload;

const hash = sjcl.hash.sha256.hash(data);
const signature = privateKey.sign(hash);
const encodedSignature = sjcl.codec.base64.fromBits(signature);

const token = data + "." + encodedSignature;

Проверка токена

const parts = token.split(".");
const data = parts[0] + "." + parts[1];

const hash = sjcl.hash.sha256.hash(data);
const signatureBits = sjcl.codec.base64.toBits(parts[2]);

const isValid = publicKey.verify(hash, signatureBits);

Подпись документов

Для документов (JSON, текст, бинарные данные) принцип аналогичен:

  1. Сериализация документа
  2. Хэширование
  3. Подпись
const document = JSON.stringify({
  contractId: 42,
  amount: 1000
});

const hash = sjcl.hash.sha256.hash(document);
const signature = privateKey.sign(hash);

Подпись хранится отдельно или вместе с документом:

{
  "document": { ... },
  "signature": "BASE64_SIGNATURE"
}

Безопасное хранение ключей

Ключевая проблема клиентской криптографии — защита приватного ключа:

  • Никогда не хранить в открытом виде
  • Использовать шифрование (например, пароль пользователя)
  • Избегать хранения в localStorage без защиты

Пример шифрования ключа:

const password = "strong_password";
const encryptedKey = sjcl.encrypt(password, JSON.stringify(privateKey));

Защита от типичных атак

Повторное использование подписи (Replay Attack)

Добавление временной метки:

payload.timestamp = Date.now();

Подмена алгоритма

Жёсткая проверка alg в header:

if (header.alg !== "ES256") throw new Error("Invalid algorithm");

Нарушение целостности

Всегда проверяется подпись перед использованием данных.


Производительность и ограничения

  • SJCL работает полностью в браузере → нагрузка на CPU клиента
  • ECC быстрее RSA при сопоставимом уровне безопасности
  • Размер подписи компактный (по сравнению с RSA)

Однако:

  • Нет встроенной поддержки форматов вроде JWS/JWT
  • Требуется ручная реализация протокола

Сравнение с Web Crypto API

Характеристика SJCL Web Crypto API
Поддержка браузеров Широкая Современные браузеры
Производительность Ниже Выше (нативная)
Простота API Средняя Сложнее
Контроль Полный Ограниченный

SJCL подходит, когда требуется:

  • Полный контроль над криптографией
  • Кросс-браузерная совместимость
  • Отсутствие зависимости от нативных API

Практические сценарии использования

  • Подпись API-запросов
  • Аутентификация без передачи пароля
  • Проверка целостности клиентских данных
  • Электронные документы и формы
  • Защита локально сохранённых данных

Расширенные подходы

Подпись нескольких полей

const data = field1 + "|" + field2 + "|" + field3;

Подпись бинарных данных

const bits = sjcl.codec.arrayBuffer.toBits(buffer);
const hash = sjcl.hash.sha256.hash(bits);

Версионирование подписи

Добавление версии схемы:

payload.version = 1;

Ошибки реализации

  • Подписание неканонизированных JSON (разный порядок полей)
  • Использование слабых кривых или параметров
  • Отсутствие проверки срока действия токена
  • Хранение приватного ключа в открытом виде
  • Использование одной пары ключей для разных задач

Канонизация данных перед подписью

Чтобы избежать проблем с различиями в форматировании:

function canonicalize(obj) {
  return JSON.stringify(obj, Object.keys(obj).sort());
}

Интеграция с сервером

Обычно:

  • Клиент подписывает данные
  • Сервер проверяет подпись
  • Сервер хранит публичный ключ

Для повышения безопасности:

  • Публичный ключ привязывается к пользователю
  • Используется HTTPS
  • Добавляется nonce или timestamp

Работа с форматами кодирования

SJCL активно использует преобразования:

  • sjcl.codec.base64
  • sjcl.codec.hex
  • sjcl.codec.utf8String

Пример:

const bits = sjcl.codec.utf8String.toBits("data");
const base64 = sjcl.codec.base64.fromBits(bits);

Управление сроком действия подписи

Добавление поля exp:

if (Date.now() > payload.exp) {
  throw new Error("Token expired");
}

Минимизация риска компрометации

  • Регулярная ротация ключей
  • Использование разных ключей для подписи и шифрования
  • Ограничение времени жизни токенов
  • Логирование попыток валидации

Расширение: подпись цепочек данных

Подпись может применяться к цепочке (например, блокам):

const blockHash = sha256(previousHash + data);

Это используется в:

  • аудит-логах
  • блокчейн-подобных структурах

Практика: полный цикл

  1. Генерация ключей
  2. Сериализация данных
  3. Хэширование
  4. Подпись
  5. Передача
  6. Проверка
  7. Валидация содержимого

Каждый этап критичен: ошибка в любом из них делает подпись бесполезной или уязвимой.