Перейти к содержанию
Learning Platform
Глоссарий Troubleshooting
Урок 08.04 · 30 мин
Продвинутый
TON ConnectAuthenticationton_proofEd25519TON

Аутентификация через ton_proof

Аутентификация через ton_proof — это способ доказать, что пользователь действительно владеет подключённым кошельком, без передачи приватного ключа. Это критически важно для любого приложения с серверной частью: без ton_proof злоумышленник может подделать адрес кошелька и получить доступ к чужому аккаунту.

В предыдущем уроке мы разобрали sendTransaction — создание блокчейн-транзакций через кошелёк. Теперь рассмотрим совершенно другой механизм: ton_proof — доказательство владения кошельком без создания транзакции.

WARNING

ton_proof — это НЕ блокчейн-транзакция

ton_proof — это чисто off-chain операция. Кошелёк подписывает структурированное сообщение приватным ключом, но ничего не записывается в блокчейн. Нет газа, нет комиссий, нет on-chain следа. Это аналог “Sign-In with Ethereum” (EIP-4361) для TON.


Зачем нужен ton_proof?

Когда пользователь подключает кошелёк через TON Connect, dApp получает его адрес. Но как убедиться, что пользователь действительно владеет этим кошельком, а не просто подставил чужой адрес?

Проблема:
  dApp получает адрес кошелька через TON Connect
  Но кто гарантирует, что отвечающий действительно владеет этим адресом?

Решение:
  ton_proof = Ed25519 подпись над структурированным сообщением
  Подпись может создать только владелец приватного ключа кошелька

Типичные сценарии использования:

  • Вход в приложение (authentication) — бэкенд выдаёт JWT после верификации
  • Привязка кошелька к аккаунту пользователя
  • Подтверждение владения перед выполнением привилегированных действий

Поток аутентификации

Frontend                   Backend                    Кошелёк
   |                          |                          |
   |-- GET /auth/payload ---->|                          |
   |<-- random payload -------|                          |
   |                          |                          |
   |-- TON Connect: ---------------------------------->  |
   |   "подпиши ton_proof     |                          |
   |    с этим payload"       |                          |
   |                          |                          |
   |<-- ton_proof + подпись --------------------------------|
   |                          |                          |
   |-- POST /auth/verify ---->|                          |
   |   (ton_proof, подпись,   |                          |
   |    публичный ключ)       |                          |
   |                          |                          |
   |<-- JWT / session --------|                          |

5 шагов

1. Бэкенд генерирует payload — случайную строку (nonce), которую привяжет к текущей сессии.

2. Frontend запрашивает ton_proof от кошелька через TON Connect, передавая payload.

3. Кошелёк формирует и подписывает структурированное сообщение с Ed25519 приватным ключом.

4. Frontend отправляет proof на бэкенд для верификации.

5. Бэкенд проверяет подпись, домен, timestamp и payload. При успехе выдаёт JWT или session cookie.


Структура ton_proof сообщения

Кошелёк подписывает структурированное сообщение, а не произвольные данные. Это предотвращает атаки с переиспользованием подписи:

Подписываемые данные (конкатенация байтов):

"ton-proof-item-v2/"                // фиксированный префикс
+ workchain (4 bytes, big-endian)   // workchain ID (0 для basechain)
+ hash (32 bytes)                   // 256-bit account hash
+ domain_length (4 bytes, LE)       // длина домена
+ domain_value (N bytes, UTF-8)     // домен dApp (например "myapp.com")
+ timestamp (8 bytes, LE)           // Unix timestamp создания
+ payload (N bytes)                 // payload от бэкенда

Поля сообщения

ПолеРазмерОписание
Префикспеременный"ton-proof-item-v2/" — защита от collision с другими подписями
Адрес36 байтWorkchain (4 байта) + hash (32 байта) — идентификация кошелька
ДоменпеременныйДомен dApp — предотвращает переиспользование на другом сайте
Timestamp8 байтВремя создания — предотвращает replay-атаки
PayloadпеременныйСлучайный nonce от бэкенда — привязка к конкретной сессии

Финальная подпись: Ed25519(private_key, SHA256(0xffff + “ton-connect” + SHA256(message)))

Двойное хеширование с префиксом 0xffff + "ton-connect" дополнительно изолирует ton_proof от любых других подписей в экосистеме TON.


Запрос ton_proof через SDK

При подключении кошелька можно сразу запросить ton_proof:

import { useTonConnectUI } from '@tonconnect/ui-react';
import { useEffect } from 'react';

function AuthProvider({ children }) {
  const [tonConnectUI] = useTonConnectUI();

  useEffect(() => {
    // Настраиваем ton_proof ДО подключения кошелька
    tonConnectUI.setConnectRequestParameters({
      state: 'ready',
      value: {
        tonProof: 'backend-generated-random-payload',
      },
    });
  }, [tonConnectUI]);

  return <>{children}</>;
}

Загрузка payload с бэкенда

В реальном приложении payload генерируется на бэкенде:

useEffect(() => {
  // Показываем загрузку пока получаем payload
  tonConnectUI.setConnectRequestParameters({ state: 'loading' });

  fetch('/api/auth/payload')
    .then(res => res.json())
    .then(({ payload }) => {
      tonConnectUI.setConnectRequestParameters({
        state: 'ready',
        value: { tonProof: payload },
      });
    })
    .catch(() => {
      // Без payload подключение без ton_proof (только адрес)
      tonConnectUI.setConnectRequestParameters(null);
    });
}, [tonConnectUI]);

Обработка ответа

После подключения проверяем наличие ton_proof в ConnectResponse:

import { useTonConnectUI } from '@tonconnect/ui-react';
import { useCallback, useEffect } from 'react';

function useAuthVerification() {
  const [tonConnectUI] = useTonConnectUI();

  const handleStatusChange = useCallback(async (wallet) => {
    if (!wallet) return; // отключён

    // Проверяем наличие ton_proof в ответе
    const tonProof = wallet.connectItems?.tonProof;
    if (!tonProof || tonProof.name !== 'ton_proof') return;

    if ('error' in tonProof) {
      console.error('ton_proof ошибка:', tonProof.error);
      return;
    }

    // Отправляем proof на бэкенд для верификации
    const response = await fetch('/api/auth/verify', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        address: wallet.account.address,
        network: wallet.account.chain,
        proof: tonProof.proof,
      }),
    });

    const { token } = await response.json();
    // Сохраняем JWT для дальнейших запросов
    localStorage.setItem('auth_token', token);
  }, []);

  useEffect(() => {
    tonConnectUI.onStatusChange(handleStatusChange);
  }, [tonConnectUI, handleStatusChange]);
}

Верификация на бэкенде

Бэкенд должен проверить 4 условия:

Проверки (все обязательны):

1. Подпись: Ed25519 verify(public_key, signature, message)
2. Домен:   proof.domain.value === "myapp.com"
3. Время:   |now - proof.timestamp| < 300 секунд
4. Payload: proof.payload === saved_payload_for_session

Псевдокод верификации

async function verifyTonProof(
  address: string,
  network: string,
  proof: TonProof
): Promise<boolean> {
  // 1. Проверяем домен
  if (proof.domain.value !== 'myapp.com') {
    return false; // proof создан для другого сайта
  }

  // 2. Проверяем timestamp (не старше 5 минут)
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - proof.timestamp) > 300) {
    return false; // proof устарел или из будущего
  }

  // 3. Проверяем payload (совпадает с выданным ранее)
  if (proof.payload !== getStoredPayload(sessionId)) {
    return false; // payload не совпадает
  }

  // 4. Получаем публичный ключ кошелька
  const publicKey = await getWalletPublicKey(address, network);

  // 5. Собираем подписанное сообщение
  const message = createTonProofMessage(address, proof);

  // 6. Проверяем Ed25519 подпись
  return ed25519.verify(publicKey, proof.signature, message);
}

Получение публичного ключа

Публичный ключ кошелька можно получить из состояния контракта через get-метод get_public_key():

async function getWalletPublicKey(
  address: string,
  network: string
): Promise<Buffer> {
  // Вызываем get_public_key() через TON API
  const result = await tonClient.runMethod(
    Address.parse(address),
    'get_public_key'
  );

  const publicKey = result.stack.readBigNumber();
  return Buffer.from(publicKey.toString(16).padStart(64, '0'), 'hex');
}
NOTE

Сравнение с Ethereum: Sign-In with Ethereum (EIP-4361)

Ethereum использует personal_sign для аутентификации — кошелёк подписывает текстовое сообщение (SIWE). TON использует структурированный бинарный формат с явным разделением полей. Обе системы решают одну задачу — off-chain доказательство владения кошельком — но с разным форматом подписи.


Защита от атак

ton_proof защищён от нескольких типов атак:

Replay-атака

Атака: злоумышленник перехватывает ton_proof и повторно отправляет на бэкенд.

Защита: Payload привязан к конкретной сессии. При повторном использовании бэкенд обнаружит, что payload уже был использован или не соответствует текущей сессии.

Cross-site атака

Атака: proof, созданный для сайта A, используется на сайте B.

Защита: Поле domain содержит домен dApp. Бэкенд проверяет совпадение с собственным доменом.

Устаревший proof

Атака: proof, созданный давно, используется сейчас.

Защита: Поле timestamp проверяется на свежесть (обычно не старше 5 минут).


Различие ton_proof и sendTransaction

Эти два механизма решают разные задачи и не должны путаться:

Аспектton_proofsendTransaction
Тип операцииOff-chain (вне блокчейна)On-chain (в блокчейн)
ЦельДоказать владение кошелькомПеревести TON / вызвать контракт
Запись в блокчейнНетДа
Газ / комиссияНетДа
ПодписьEd25519 над структурированным сообщениемEd25519 над BOC транзакции
РезультатProof для верификации бэкендомПодписанный BOC для сети
Типичное использованиеЛогин, привязка кошелькаПеревод, swap, mint

Итоги

КомпонентРоль
ton_proofOff-chain доказательство владения кошельком через Ed25519 подпись
PayloadСлучайный nonce от бэкенда — привязка к сессии, защита от replay
DomainДомен dApp — защита от cross-site переиспользования proof
TimestampВремя создания — защита от устаревших proof
Ed25519Алгоритм подписи — тот же ключ, что и для on-chain транзакций
get_public_key()Get-метод контракта кошелька для получения публичного ключа

В следующем уроке мы соберём всё вместе — разберём React SDK @tonconnect/ui-react в деталях: все хуки, компоненты, кастомизацию и паттерны обработки ошибок.


Частые ошибки

  1. Проверяют ton_proof только на клиенте: верификация подписи должна выполняться на сервере, иначе злоумышленник обойдёт проверку.
  2. Не включают timestamp в ton_proof запрос: без проверки времени злоумышленник может использовать перехваченный proof повторно.
  3. Не верифицируют, что public key из proof соответствует адресу кошелька, что позволяет подменить кошелёк.
  4. Забывают проверять domain в proof: без этой проверки proof, выданный для одного сайта, может быть использован на другом.

Проверка знанийKnowledge check
Почему ton_proof включает поле domain, и от какой атаки оно защищает?
ОтветAnswer
Поле domain содержит домен dApp (например 'myapp.com'), для которого создан proof. Это защищает от cross-site атаки: если злоумышленник перехватит ton_proof, созданный для сайта A, и попытается использовать его на сайте B, бэкенд сайта B обнаружит, что domain в proof не совпадает с его собственным доменом, и отклонит аутентификацию.

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5