Skip to Content
ПосібникиМультичейн-автентифікація з SIWX

Мультичейн-автентифікація з SIWX (CAIP-122 за межами EVM-силосів)

Тривалий час стандартом криптографічної ідентифікації у Web3 був EIP-4361 (Sign-In with Ethereum / SIWE). Він дозволив безпечно входити за допомогою адреси гаманця Ethereum без паролів або соціальних провайдерів.

Проте сучасні децентралізовані додатки майже ніколи не обмежуються однією мережею. Коли проєкт масштабується на EVM-ролапи та швидкісні середовища без EVM (зокрема Solana), розробники неминуче стикаються з критичними структурними обмеженнями:

  • EVM-авторизація жорстко прив'язана до кривої secp256k1 та стандарту SIWE.
  • Solana-авторизація вимагає саморобних перевірок ed25519 або підпису сирого тексту в Base58.
  • Бекенд змушений підтримувати фрагментовані сховища сесій, різні формати кук та несумісні конвеєри валідації.

Щоб подолати цю фрагментацію, екосистема прийняла відкритий стандарт CAIP-122 (Sign-In with X).


1. Архітектурна проблема: одночейнові силоси

Основне обмеження EIP-4361 — жорстка прив'язка до примітивів Ethereum: відновлення відкритого ключа з secp256k1, hex-кодування та числові chain ID (eip155:1). Спроба додати Solana поруч з EVM породжує два паралельні, незв'язані стеки автентифікації:

┌────────────────────────────────────────────────────────┐│ Divergent Auth Spaghetti │├───────────────────────────┬────────────────────────────┤│ EVM Stack │ Solana Stack │├───────────────────────────┼────────────────────────────┤│ • EIP-4361 Message Format │ • Arbitrary raw text/JSON ││ • viem / ethers verify │ • tweetnacl / ed25519 hook ││ • Hex Address Encoding │ • Base58 Address Encoding ││ • Custom Session Cookie A │ • Custom Session Cookie B │└───────────────────────────┴────────────────────────────┘

Критичні пастки безпеки фрагментованої автентифікації

  • Збій у перевірці прав доступу: бекенд-маршрути не можуть надійно гарантувати, чи має активна сесія право викликати чутливі операції між різними мережами.
  • Підміна нонсів (Cross-Chain Nonce Poisoning): без ізоляції просторів імен (namespaces) виклики нонсів для Ethereum можуть бути повторно використані або перехоплені в транзакціях Solana.
  • Подвійне навантаження на підтримку: дві паралельні схеми кук, механізми захисту від CSRF та подвійні ендпоінти виходу експоненційно збільшують поверхню потенційної атаки.

2. Стандарт CAIP-122 (Sign-In with X)

CAIP-122 уніфікує аутентифікацію за допомогою агностичних ідентифікаторів CAIP-2 (ідентифікатор мережі) та CAIP-10 (повна адреса облікового запису):

eip155:1:0x1234567890abcdef1234567890abcdef12345678 <-- Ethereum Mainnet (CAIP-10)solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:9WzDXwBbmkg... <-- Solana Mainnet (CAIP-10)

Життєвий цикл автентифікації

Повний наскрізний життєвий цикл автентифікації — від генерації challenge-нонсу до мультичейн-верифікації криптографічного підпису та атомарного карбування сесії із захистом від повторного відтворення (anti-replay):

Обов'язковий перепідпис при зміні акаунта або мережі:Криптографічний доказ CAIP-122 суворо пов'язує поточну сесію з конкретною адресою та мережею. Коли користувач змінює активний гаманець чи мережу в розширенні, новий акаунт зобов'язаний пройти підпис наново.

3. Модульна архітектура SIWX: пакети та адаптери

Модульна архітектура TUWA SIWX розділена на незалежні пакети з нульовими непотрібними залежностями: @tuwaio/siwx-core (ядро без залежностей), @tuwaio/siwx-evm (підтримка EIP-191, EIP-1271, ERC-6492), @tuwaio/siwx-solana (підтримка ed25519 та Wallet Standard) і @tuwaio/siwx-server (серверні сховища Redis/Stateless та валідація).

┌────────────────────────────────────────────────────────────────────────┐│ TUWA SIWX Modular Architecture │├────────────────────────────────────────────────────────────────────────┤│ L1: Pure Core Engine ││ • @tuwaio/siwx-core CAIP-122 Builder & Parser (+orbit-core) │├────────────────────────────────────────────────────────────────────────┤│ L2: Network Adapters (Multi-Chain Primitives) ││ • @tuwaio/siwx-evm EVM: EIP-191 / EIP-1271 / ERC-6492 ││ • @tuwaio/siwx-solana Solana: @solana/kit / SubtleCrypto │├────────────────────────────────────────────────────────────────────────┤│ L2: Presentation & Bindings ││ • @tuwaio/siwx-react React Hooks & Zustand Session Store ││ • @tuwaio/siwx-server Verifier, Next.js Handlers, JWT + JWKS │└────────────────────────────────────────────────────────────────────────┘

4. Порівняння мультичейн-адаптерів

Адаптери мереж інкапсулюють специфіку відповідних віртуальних машин та криптографічних підписів, транслюючи їх у єдиний інтерфейс SiwxSigner на фронтенді та SiwxVerifier на бекенді:

Технічний векторEVM-адаптер (@tuwaio/siwx-evm)Solana-адаптер (@tuwaio/siwx-solana)
CAIP-2 Namespaceeip155:{chainId} (наприклад, eip155:1)solana:{genesisHash} (наприклад, solana:5eykt4Us...)
Формат адреси20-байтний Hex (0x[a-fA-F0-9]{40})32-байтний Base58 ([1-9A-HJ-NP-za-km-z]{32,44})
Повний CAIP-10 IDeip155:1:0xAb5801a7D398351b8bE11C439e05C5B3259aeC9Bsolana:5eykt4Us...:9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM
Криптографічна криваsecp256k1 (Koblitz curve ECDSA)ed25519 (Edwards-curve Digital Signature)
Формат підпису65-байтний Hex-рядок (0x + 130 симв: r, s, v)64-байтний Base58-рядок (88 символів)
Підтримувані гаманціMetaMask, Rabby, Rainbow, Coinbase, Safe та всі стандарту EIP-1193Phantom, Solflare, Backpack, OKX та всі Wallet Standard гаманці
Смарт-контрактні акаунтиEIP-1271 (розгорнуті) та ERC-6492 (ще не розгорнуті) на ланцюгу підписуНе підтримується (тільки Keypairs; PDA є поза кривою й не мають приватних ключів)
Механізм верифікаціїViem recoverAddress (EIP-191) + verifyMessage (EIP-1271 / ERC-6492)Нативний Web Crypto API crypto.subtle.verify (Ed25519, нуль залежностей)

5. Наскрізне впровадження в екосистемі TUWA

Крок 1: Клієнтський мультичейн-вхід

Програмна кнопка входу із Satellite Connect

Зручна кнопка входу підписує активний гаманець — EVM чи Solana. Утиліта getSatelliteSiwxFields конвертує з'єднання у CAIP-2 chain ID та CAIP-10 адресу:

// src/components/SignInButton.tsx'use client'; import { createEvmSiwxSigner } from '@tuwaio/evm-sdk/siwx';import { useSatelliteConnectStore } from '@tuwaio/sdk/satellite';import { createSatelliteSiwxSigner, getSatelliteSiwxFields, isSessionMatchingConnection, useSiwx, useSiwxSession,} from '@tuwaio/sdk/siwx';import type { SolanaConnection } from '@tuwaio/solana-sdk/satellite'; import { wagmiConfig } from '@/configs/appConfig'; export function SignInButton() { const activeConnection = useSatelliteConnectStore((state) => state.activeConnection); const { signIn, signOut } = useSiwx(); const { session, status } = useSiwxSession(); if (!activeConnection?.isConnected) return null; // The SIWX helpers read the account and the chain of the connection const account = { address: activeConnection.address, chainId: activeConnection.chainId }; // The session was issued for this account and chain: an account or network switch shows "Sign in" again if (isSessionMatchingConnection(session, account)) { return ( <button onClick={async () => { await fetch('/api/siwx/logout', { method: 'POST' }); signOut(); }} > Sign out </button> ); } const handleSignIn = async () => signIn({ // EVM wallets sign through wagmi; a Solana connection carries the signMessage of its Wallet Standard wallet signer: activeConnection.connectorType.startsWith('evm:') ? createEvmSiwxSigner(wagmiConfig) : await createSatelliteSiwxSigner(activeConnection as SolanaConnection), getNonce: async () => { const res = await fetch('/api/siwx/nonce'); return ((await res.json()) as { nonce: string }).nonce; }, verifier: async (payload) => { const res = await fetch('/api/siwx/verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); return res.ok ? res.json() : null; }, // eip155:11155111:0x… for an EVM wallet, solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1:… for a Solana devnet one fields: getSatelliteSiwxFields(account, { statement: 'Sign in to access TUWA sovereign services.' }), onSuccess: (session) => console.log('[SIWX] Signed in:', session.address), }); return ( <button onClick={handleSignIn} disabled={status !== 'idle' && status !== 'error'}> Sign in </button> );}

Декларативний патерн Watcher із Satellite та Nova UI Kit

Для додатків на базі Nova UIKit використовується декларативний патерн з компонентами-watcher:

// src/providers/SatelliteConnectProviders.tsx'use client'; import { EVMConnectorsWatcher } from '@tuwaio/evm-sdk/nova-connect';import { satelliteEVMAdapter } from '@tuwaio/evm-sdk/satellite';import { NovaConnectProvider } from '@tuwaio/sdk/nova-connect';import { SatelliteConnectProvider } from '@tuwaio/sdk/nova-connect/satellite';import { SolanaConnectorsWatcher } from '@tuwaio/solana-sdk/nova-connect';import { satelliteSolanaAdapter } from '@tuwaio/solana-sdk/satellite'; import { appEVMChains, solanaRPCUrls, wagmiConfig } from '@/configs/appConfig'; export function SatelliteConnectProviders({ children }: { children: React.ReactNode }) { return ( <SatelliteConnectProvider adapter={[satelliteEVMAdapter(wagmiConfig, appEVMChains), satelliteSolanaAdapter({ rpcUrls: solanaRPCUrls })]} autoConnect={true} > {/* The watchers follow account and network changes made in the wallet */} <EVMConnectorsWatcher wagmiConfig={wagmiConfig} /> <SolanaConnectorsWatcher /> <NovaConnectProvider appChains={appEVMChains} solanaRPCUrls={solanaRPCUrls} siwx={{ expirationSeconds: 1800, getNonce: async () => { const res = await fetch('/api/siwx/nonce'); return ((await res.json()) as { nonce: string }).nonce; }, verifier: async (payload) => { const res = await fetch('/api/siwx/verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); return res.ok ? res.json() : null; }, destroyer: async () => { await fetch('/api/siwx/logout', { method: 'POST' }); }, }} > {children} </NovaConnectProvider> </SatelliteConnectProvider> );}

Крок 2: Серверна криптографічна верифікація

Пакет @tuwaio/siwx-server повністю агностичний до бекенд-фреймворку: він однаково надійно працює в Next.js App Router, Fastify, NestJS, Cloudflare Workers, Hono чи Express.

Продакшн Route Handler (атомарний Redis Nonce та сесії)

// src/app/api/siwx/[...siwx]/route.tsimport type { SiwxNonceStore, SiwxSession, SiwxSessionRecord, SiwxSessionStore } from '@tuwaio/sdk/siwx/server';import { createSiwxApiHandler } from '@tuwaio/sdk/siwx/server-next'; import { redisClient } from '@/lib/redis'; // Atomic Redis session store conforming to SiwxSessionStoreconst sessionStore: SiwxSessionStore = { async get(id: string): Promise<SiwxSessionRecord | null> { const data = await redisClient.get(`siwx:sess:${id}`); return data ? (JSON.parse(data) as SiwxSessionRecord) : null; }, async create({ session, ttlSeconds }: { session: SiwxSession; ttlSeconds: number }): Promise<SiwxSessionRecord> { const id = crypto.randomUUID(); const createdAt = Date.now(); const expiresAt = createdAt + ttlSeconds * 1000; const record: SiwxSessionRecord = { id, session, createdAt, expiresAt }; await redisClient.set(`siwx:sess:${id}`, JSON.stringify(record), 'EX', ttlSeconds); return record; }, // Links the wallet session to your own user record after sign-in; SIWX itself never calls it async bindSubject(id: string, subjectId: string): Promise<boolean> { const data = await redisClient.get(`siwx:sess:${id}`); if (!data) return false; const record = { ...(JSON.parse(data) as SiwxSessionRecord), subjectId }; await redisClient.set(`siwx:sess:${id}`, JSON.stringify(record), 'KEEPTTL'); return true; }, async revoke(id: string): Promise<void> { await redisClient.del(`siwx:sess:${id}`); },}; // Atomic Redis nonce store preventing replay attacksconst nonceStore: SiwxNonceStore = { async issue({ nonce, ttlSeconds }: { nonce: string; ttlSeconds: number }) { await redisClient.set(`siwx:nonce:${nonce}`, '1', 'EX', ttlSeconds); }, async consume({ nonce }: { nonce: string }) { const result = await redisClient.del(`siwx:nonce:${nonce}`); return result === 1; // Atomic consumption prevents double-spend/replay },}; const handler = createSiwxApiHandler({ sessionStore, nonceStore, policy: { expectedDomain: process.env.NEXT_PUBLIC_APP_DOMAIN!, requireExpirationTime: true, maxIssuedAtAgeSeconds: 300, // 5 minutes maxSessionLifetimeSeconds: 60 * 60 * 24 * 7, // 7 days clockSkewSeconds: 60, }, cookieOptions: { name: 'siwx-session', secure: process.env.NODE_ENV === 'production', },}); export const { GET, POST, DELETE } = handler;

Stateless Route Handler без інфраструктури (HMAC-підписаний токен)

// src/app/api/siwx/[...siwx]/route.tsimport { createStatelessDemoSiwxHandler } from '@tuwaio/sdk/siwx/server-next'; const handler = createStatelessDemoSiwxHandler({ signingSecret: process.env.SIWX_SIGNING_SECRET!, // Minimum 32 characters policy: { expectedDomain: ['localhost:3000', process.env.NEXT_PUBLIC_APP_DOMAIN!], requireExpirationTime: true, maxSessionLifetimeSeconds: 1800, // 30 minutes }, cookieOptions: { name: 'siwx-demo-session', secure: process.env.NODE_ENV === 'production', },}); export const { GET, POST, DELETE } = handler;

Крок 3: Захист Server Actions та бекенд-викликів

Функція isSessionMatchingTarget забезпечує еквівалентність крос-чейн адрес: вона нормалізує EVM hex, чексуми, Solana Base58 та повні рядки CAIP-10, унеможливлюючи неавторизовані операції:

// src/app/actions/transaction.ts'use server'; import { getSiwxServerSession, isSessionMatchingTarget } from '@tuwaio/sdk/siwx/server';import { cookies } from 'next/headers'; import { sessionStore } from '@/lib/authStores'; export async function submitProtectedOperation({ targetAddress, chainId, actionPayload,}: { targetAddress: string; chainId: string; actionPayload: Record<string, unknown>;}) { const cookieStore = await cookies(); // 1. Resolve active session from durable store or signed cookie const session = await getSiwxServerSession({ cookieSource: cookieStore, cookieName: 'siwx-session', sessionStore, // Or `signingSecret` when using stateless profile policy: { expectedDomain: process.env.NEXT_PUBLIC_APP_DOMAIN!, }, }); if (!session) { throw new Error('Unauthorized: No active or valid SIWX session found.'); } // 2. Cryptographic claim verification: assert wallet ownership matches target const isMatch = isSessionMatchingTarget(session, targetAddress, chainId); if (!isMatch) { throw new Error( `Forbidden: Session wallet (${session.address}) does not match transaction sender (${targetAddress}).`, ); } // 3. Proceed with authorized operation console.log(`[Authorized] Dispatched for ${session.address} on ${session.chainId}`); return { success: true, processedAt: new Date().toISOString() };}

6. Порівняння: EIP-4361 проти CAIP-122 (SIWX)

Архітектурний вимірEIP-4361 (SIWE)CAIP-122 (@tuwaio/siwx)
Охоплення мережТільки Ethereum та EVM ролапиМультичейн (EVM, Solana; розширюється через CAIP-2)
Ідентифікація мережіЧисловий Chain ID (1, 8453)Стандарт CAIP-2 (eip155:1, solana:5eykt...)
Кодування адресиСуворий Hex-формат (0x...)Мультиформат: Hex (EVM) та Base58 (Solana) через CAIP-10
Схеми підписуsecp256k1 (personal_sign / EIP-1271)secp256k1 (EIP-191 / EIP-1271 / ERC-6492) та ed25519
Модель сесіїІзольовані EVM-кукиЄдина уніфікована схема сесії (SiwxSession)
Захист від повторів (Replay)Саморобні перевірки на серверіАтомарне сховище одноразових нонсів (SiwxNonceStore)
Зовнішня автентифікаціяНе входить до стандартуJWT + JWKS для зовнішніх систем (@tuwaio/siwx-server)

Висновки

Створення окремих рішень для EVM та non-EVM середовищ фрагментує стан сесій та створює вразливості авторизації. Використання стандарту CAIP-122 через @tuwaio/siwx дозволяє dApps побудувати надійний безкастодіальний контур безпеки, де кожний підпис верифікується на власному бекенді за єдиною прозорою політикою.


📚 Наступні кроки

Востаннє оновлено