Сравнение

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

TON и Tycho совместимы на уровне TVM и поэтому используют похожие примитивы проверки подписи. Отличия появляются из-за разных требований к безопасности при существовании нескольких сетей, где пользователи могут переиспользовать один и тот же ключ и один и тот же контракт. В Tycho добавлен контекст сети в данные, которые проверяются подписью, чтобы убрать риск replay-атак между сетями.

Краткое сравнение

Что сравниваемTONTycho
Что фактически проверяется подписьюОбычно подпись над 256-битным хэшем сообщенияПодпись над теми же данными, но с добавлением домена подписи, который зависит от global_id сети
Базовые примитивы в VMCHKSIGNU для проверки подписи хэша, CHKSIGNS для проверки подписи данныхТе же опкоды, но поведение меняется при включённой поддержке домена подписи
Контекст сети в подписиОтсутствуетДобавляется на уровне VM как префикс к данным проверки, по умолчанию для L2 сетей

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

В TON стандартная схема для контрактов и кошельков выглядит так:

  • офчейн код сериализует подписываемые поля в ячейку
  • вычисляет 256-битный хэш ячейки
  • подписывает этот хэш алгоритмом Ed25519
  • контракт на чейне пересчитывает хэш и проверяет подпись

TVM предоставляет два варианта проверки подписи:

  • CHKSIGNU проверяет подпись хэша
  • CHKSIGNS проверяет подпись данных

На практике чаще используют проверку по хэшу через CHKSIGNU, потому что CHKSIGNS проверяет только данные из одной ячейки и игнорирует ссылки. Если подписываемая структура содержит несколько ячеек или ссылки, сначала считают хэш всей структуры, а затем проверяют подпись хэша.

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

Tycho добавляет понятие домена подписи, чтобы одна и та же полезная нагрузка приводила к разным данным для подписи в разных сетях. Домен подписи связан с идентификатором сети global_id.

TL схема домена подписи

// Non-empty variant. Hash of its TL representation
// is used as a prefix for the verified data.
signature_domain.l2#71b34ee1 global_id:int = SignatureDomain;

// Special variant to NOT add any prefix for the verified data.
// Can be used to verify mainnet signatures from L2 networks.
signature_domain.empty#e1d571b = SignatureDomain;

Как домен подписи влияет на проверку

В VM появляется стек доменов подписи. Пустой стек эквивалентен пустому домену подписи. Текущий домен подписи влияет на CHKSIGNU и CHKSIGNS так:

  • если домен пустой, проверка совпадает с исходным поведением и префикс не добавляется
  • если домен непустой и включена capability SignatureDomain, к данным проверки добавляется 32-байтный префикс, который является хэшем TL-представления домена подписи

Для L2 сетей Tycho домен подписи добавляется в стек неявно перед исполнением контракта. Это означает, что контракт, который в TON успешно проверял подпись, в Tycho может начать получать false, если подпись создавалась без учёта домена подписи.

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

Для работы с доменом подписи в Tycho добавлены опкоды, которых нет в TVM в сети TON.

Новые опкоды и их семантика из sources/SignatureDomain.md:

  • f91800 SIGNDOMAIN (- x или ⊥) кладёт на стек текущий домен подписи, где x это global_id, либо ⊥ если домена подписи не было
  • f91801 SIGNDOMAIN_POP (- x или ⊥) делает то же самое, но дополнительно удаляет считанное значение из стека доменов подписи
  • f91802 SIGNDOMAIN_PUSH (x или ⊥ -) добавляет опциональный global_id в стек доменов подписи и делает его текущим доменом, диапазон -2^31 ≤ x < 2^31

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

Рекомендации для разработчиков

  • При переносе контракта из TON в Tycho не нужно менять код контракта и использовать новые опкоды SIGNDOMAIN*. Новое поведение реализовано на уровне VM в CHKSIGNU и CHKSIGNS и включается через capability SignatureDomain. Этот режим обычно включён по умолчанию через параметры сети.
  • При переносе TON контракта, который проверяет подписи, проверьте, что подпись формируется в том же контексте global_id, который использует сеть Tycho. Если подпись создаётся старой библиотекой без домена подписи, проверка в контракте может начать падать.
  • Если контракту нужно принимать подписи из разных доменов, закладывайте это в протокол сообщения и верификацию. Например, перед проверкой подписи переключайте домен подписи через SIGNDOMAIN_PUSH, либо используйте пустой домен для проверок без префикса.
  • Тестируйте негативные сценарии. Одна и та же подпись должна быть невалидной при другом global_id, если вы рассчитываете на защиту от replay между сетями.
  • Изменения домена подписи относятся к базовой проверке Ed25519 через CHKSIGNU и CHKSIGNS. Проверка подписей для алгоритмов отличных от Ed25519, включая BLS, не имплементирована в Tycho VM. Поэтому описываемая межсетевая replay-атака через BLS подписи для Tycho не актуальна.

Пример формирования подписи с учётом включения домена подписи в Tycho. Пример использует global_id и capability бит SignatureDomain, которые берутся из конфигурации сети.

const params = Dictionary.loadDirect(
  Dictionary.Keys.Uint(32),
  Dictionary.Values.Cell(),
  // Root cell of a non-empty config params dict
  configRootFromTheCurrentNetwork,
);

// 19 param is required in tycho networks, and might be null in others
const globalId = params.get(19)?.asSlice().loadInt(32);
// 8 param is always set
const capabilities = params.get(8)!.asSlice().skip(8 + 32).loadUintBig(64);

const SIG_DOMAIN_CAPABILITY_BIT = 0x800000000n;

const domain: SignatureDomain =
  globalId != null && (capabilities & SIG_DOMAIN_CAPABILITY_BIT) != 0n
    ? { type: "l2", globalId }
    : { type: "empty" };

const signature = domainSign({ data, secretKey, domain });

Пример расчёта данных для проверки подписи в CHKSIGNU с доменом подписи. Важно, что в данных для проверки участвует префикс домена подписи и 32-байтное представление проверяемого числа.

global_id = 1000 # 0x000003e8
prefix = sha256(
  0x71b34ee1.to_bytes(4, byteorder="little")
  + global_id.to_bytes(4, byteorder="little")
)
data = 0xdeadbeef
signature = ...
public_key = ...

data_to_check = prefix + data.to_bytes(32, byteorder="big")
ed25519_verify(data_to_check, signature, public_key)