nacl.sign.detached.verify: проверка отделённой подписи

Назначение и модель работы

Механизм detached signature в TweetNaCl.js реализует схему цифровой подписи Ed25519, где подпись хранится отдельно от сообщения. Это принципиально отличает её от «упакованных» форматов, где сообщение и подпись объединены в один буфер.

Функция nacl.sign.detached.verify выполняет криптографическую проверку: соответствует ли переданная подпись конкретному сообщению и публичному ключу.

Проверка происходит в одну сторону: восстановить приватный ключ или изменить сообщение так, чтобы подпись осталась валидной, вычислительно невозможно при корректной реализации алгоритма.


Сигнатура функции

nacl.sign.detached.verify(message, signature, publicKey)

Параметры

message: Uint8Array

Исходное сообщение в бинарном виде. Важно: проверка выполняется строго над байтами, а не строкой.

signature: Uint8Array (64 байта)

Отделённая цифровая подпись, сформированная функцией:

nacl.sign.detached(message, secretKey)

publicKey: Uint8Array (32 байта)

Публичный ключ Ed25519, соответствующий приватному ключу, которым была создана подпись.


Возвращаемое значение

boolean
  • true — подпись корректна, сообщение не изменено и принадлежит владельцу ключа
  • false — подпись невалидна, сообщение изменено или ключ не соответствует подписи

Базовый пример использования

import nacl from "tweetnacl";

// генерация ключевой пары
const keyPair = nacl.sign.keyPair();

// сообщение
const message = new TextEncoder().encode("secure message");

// подпись
const signature = nacl.sign.detached(message, keyPair.secretKey);

// проверка подписи
const isValid = nacl.sign.detached.verify(
  message,
  signature,
  keyPair.publicKey
);

console.log(isValid); // true

Ключевые свойства проверки

1. Проверка не восстанавливает сообщение

Функция не выполняет расшифровку и не модифицирует данные. Она только сравнивает криптографическую целостность.

2. Не зависит от источника подписи

Подпись может прийти из сети, файла, базы данных — проверка всегда сводится к математическому сравнению.

3. Константное время исполнения

Реализация TweetNaCl.js построена так, чтобы исключать утечки через timing attacks. Время выполнения не зависит от содержимого сообщения или подписи.


Внутренняя логика проверки

Алгоритм Ed25519 в режиме detached verification:

  1. Использует публичный ключ для восстановления точек на эллиптической кривой
  2. Пересчитывает хэш от сообщения и параметров подписи
  3. Сравнивает вычисленный результат с компонентами подписи
  4. Возвращает булево значение без раскрытия промежуточных данных

Важно: на уровне API никакие промежуточные значения не доступны.


Типы данных и частые ошибки

Ошибка 1: передача строки вместо Uint8Array

// ❌ неправильно
nacl.sign.detached.verify(
  "message",
  signature,
  publicKey
);

Правильно:

const message = new TextEncoder().encode("message");

Ошибка 2: несовпадение кодировки

Если подпись создавалась от UTF-8 байтов, а проверка идёт от другой кодировки (например, Latin-1), результат всегда будет false.


Ошибка 3: повреждённая подпись

Подпись должна быть строго 64 байта:

signature.length === 64

Любое изменение даже одного байта делает подпись недействительной.


Ошибка 4: несоответствие ключа

Публичный ключ должен соответствовать приватному ключу, которым создана подпись. Проверка с чужим ключом всегда возвращает false.


Работа с бинарными форматами

В реальных приложениях данные часто приходят не в виде Uint8Array, а в виде:

  • Base64
  • Hex
  • ArrayBuffer
  • JSON строк

Пример: Base64 → Uint8Array

function base64ToUint8Array(base64) {
  const binary = atob(base64);
  const bytes = new Uint8Array(binary.length);

  for (let i = 0; i < binary.length; i++) {
    bytes[i] = binary.charCodeAt(i);
  }

  return bytes;
}

Использование:

const signature = base64ToUint8Array(sigBase64);
const publicKey = base64ToUint8Array(pubKeyBase64);
const message = new TextEncoder().encode(text);

const ok = nacl.sign.detached.verify(message, signature, publicKey);

Проверка сообщений в протоколах

Функция широко используется в прикладных системах:

  • проверка JWT-подобных структур (но на уровне ниже JWT)
  • подпись сообщений в peer-to-peer системах
  • валидация запросов API
  • подтверждение транзакций
  • защита webhook-событий

Безопасная схема использования

Типичный безопасный поток:

  1. Сервер генерирует ключевую пару
  2. Приватный ключ хранится изолированно
  3. Сообщение подписывается через nacl.sign.detached
  4. Клиент получает (message, signature, publicKey)
  5. Клиент вызывает nacl.sign.detached.verify
  6. Результат используется как условие доверия

Важные ограничения модели

1. Нет частичной верификации

Невозможно проверить «часть подписи» или «часть сообщения».

2. Нет восстановления данных

Подпись не содержит информации о сообщении.

3. Нет встроенного хеширования строки

Хеширование выполняется внутри алгоритма Ed25519. Дополнительный SHA-256 на стороне приложения меняет результат проверки.


Сравнение с nacl.sign.detached

Операция Назначение
nacl.sign.detached создание подписи
nacl.sign.detached.verify проверка подписи

Обе функции являются парными и должны использовать один и тот же формат входных данных.


Поведение при ошибках

Функция не выбрасывает исключения при неверной подписи. Она всегда возвращает false.

Это важно для безопасности:

  • отсутствие stack trace
  • отсутствие утечки причин отказа
  • единообразное поведение при любых ошибках

Пример проверки данных из сети

async function verifyPayload(payload, publicKey) {
  const message = new TextEncoder().encode(payload.message);

  const signature = base64ToUint8Array(payload.signature);

  return nacl.sign.detached.verify(
    message,
    signature,
    publicKey
  );
}

Типичные архитектурные ошибки

Использование строки вместо байтов

Даже одинаковая строка может иметь разные байтовые представления.

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

new TextEncoder().encode(new TextEncoder().encode(str))

даёт некорректный результат.

Изменение сообщения после подписи

Любая модификация:

  • пробелы
  • переносы строк
  • нормализация Unicode

ломает проверку.


Особенности работы с Unicode

JavaScript строки не являются бинарным форматом. TextEncoder всегда преобразует строку в UTF-8.

Это значит:

  • одинаковые символы ≠ одинаковые байты при разных нормализациях
  • визуально одинаковые строки могут давать разные подписи

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

Для корректной работы проверки необходимо соблюдение трёх условий:

  • сообщение идентично побайтово
  • подпись имеет длину 64 байта
  • публичный ключ соответствует ключевой паре подписи