Аутентификация через ton_proof
Аутентификация через ton_proof — это способ доказать, что пользователь действительно владеет подключённым кошельком, без передачи приватного ключа. Это критически важно для любого приложения с серверной частью: без ton_proof злоумышленник может подделать адрес кошелька и получить доступ к чужому аккаунту.
В предыдущем уроке мы разобрали sendTransaction — создание блокчейн-транзакций через кошелёк. Теперь рассмотрим совершенно другой механизм: ton_proof — доказательство владения кошельком без создания транзакции.
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 — предотвращает переиспользование на другом сайте |
| Timestamp | 8 байт | Время создания — предотвращает 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');
}
Сравнение с 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_proof | sendTransaction |
|---|---|---|
| Тип операции | Off-chain (вне блокчейна) | On-chain (в блокчейн) |
| Цель | Доказать владение кошельком | Перевести TON / вызвать контракт |
| Запись в блокчейн | Нет | Да |
| Газ / комиссия | Нет | Да |
| Подпись | Ed25519 над структурированным сообщением | Ed25519 над BOC транзакции |
| Результат | Proof для верификации бэкендом | Подписанный BOC для сети |
| Типичное использование | Логин, привязка кошелька | Перевод, swap, mint |
Итоги
| Компонент | Роль |
|---|---|
| ton_proof | Off-chain доказательство владения кошельком через Ed25519 подпись |
| Payload | Случайный nonce от бэкенда — привязка к сессии, защита от replay |
| Domain | Домен dApp — защита от cross-site переиспользования proof |
| Timestamp | Время создания — защита от устаревших proof |
| Ed25519 | Алгоритм подписи — тот же ключ, что и для on-chain транзакций |
| get_public_key() | Get-метод контракта кошелька для получения публичного ключа |
В следующем уроке мы соберём всё вместе — разберём React SDK @tonconnect/ui-react в деталях: все хуки, компоненты, кастомизацию и паттерны обработки ошибок.
Частые ошибки
- Проверяют ton_proof только на клиенте: верификация подписи должна выполняться на сервере, иначе злоумышленник обойдёт проверку.
- Не включают timestamp в ton_proof запрос: без проверки времени злоумышленник может использовать перехваченный proof повторно.
- Не верифицируют, что public key из proof соответствует адресу кошелька, что позволяет подменить кошелёк.
- Забывают проверять domain в proof: без этой проверки proof, выданный для одного сайта, может быть использован на другом.
Проверка знанийПочему ton_proof включает поле domain, и от какой атаки оно защищает?
Проверьте понимание
Закончили урок?
Отметьте его как пройденный, чтобы отслеживать свой прогресс
Войдите чтобы оценить урок