
Assinando Mensagens
Nem toda interação precisa de uma transação. Às vezes você só precisa provar que um usuário controla uma carteira, sem escrever nada na blockchain ou gastar lamports.
A assinatura de mensagens permite fluxos de autenticação, atestações off-chain e provas criptográficas de propriedade. O Sign In With Solana (SIWS) padroniza isso para login web3, substituindo email/senha por assinaturas de carteira.
Nesta lição, você criará um hook useSignMessage e aprenderá como implementar Sign In With Solana para autenticação.
Como a Assinatura de Mensagens Funciona
Quando você assina uma transação, está autorizando uma mudança de estado na Solana. Quando assina uma mensagem, está criando uma prova criptográfica que diz "esta carteira aprovou este conteúdo exato."
A assinatura é determinística: a mesma carteira assinando a mesma mensagem sempre produzirá a mesma assinatura. Qualquer pessoa com a chave pública pode verificar que a assinatura foi feita pela chave privada correspondente, sem nunca ver essa chave privada.
Mensagem + Chave Privada → Assinatura
Mensagem + Assinatura + Chave Pública → Verdadeiro/Falso (verificação)Isso é criptografia Ed25519, o mesmo esquema que a Solana usa para assinaturas de transações.
Assinatura Básica de Mensagens
O método signMessages recebe um array de payloads (arrays de bytes) e retorna assinaturas:
import { transact } from '@solana-mobile/mobile-wallet-adapter-protocol-web3js';
import { toByteArray } from 'react-native-quick-base64';
async function signMessage(message: string): Promise<Uint8Array> {
return await transact(async (wallet) => {
const authResult = await wallet.authorize({
identity: APP_IDENTITY,
chain: 'solana:mainnet',
});
// Converter string para bytes
const messageBytes = new TextEncoder().encode(message);
// Assinar a mensagem
const signatures = await wallet.signMessages({
addresses: [authResult.accounts[0].address],
payloads: [messageBytes],
});
// signatures[0] é Uint8Array (64 bytes)
return signatures[0];
});
}Pontos importantes:
addresses: Quais conta(s) devem assinarpayloads: Os bytes brutos para assinarRetorna: Array de assinaturas correspondendo aos payloads
A carteira mostra aos usuários o conteúdo da mensagem (decodificado como UTF-8 se possível) e pede aprovação.
Assinando Múltiplas Mensagens
Você pode assinar múltiplas mensagens em uma solicitação:
const signatures = await wallet.signMessages({
addresses: [account.address, account.address],
payloads: [messageBytes1, messageBytes2],
});
// signatures[0] corresponde a messageBytes1
// signatures[1] corresponde a messageBytes2Verificando Assinaturas
Após receber uma assinatura, você (ou seu backend) pode verificá-la:
import { sign } from 'tweetnacl';
import { PublicKey } from '@solana/web3.js';
function verifySignature(
message: Uint8Array,
signature: Uint8Array,
publicKey: PublicKey
): boolean {
return sign.detached.verify(
message,
signature,
publicKey.toBytes()
);
}
// Uso
const message = new TextEncoder().encode('Olá, Solana!');
const signature = await signMessage('Olá, Solana!');
const publicKey = /* chave pública do usuário da autorização */;
const isValid = verifySignature(message, signature, publicKey);
console.log('Assinatura válida:', isValid); // trueImportante: Você deve verificar contra exatamente os mesmos bytes que foram assinados. Se você codificar a mensagem de forma diferente (codificação diferente, espaços em branco, quebras de linha), a verificação falhará.
Sign In With Solana
Sign In With Solana (SIWS) é um padrão para autenticação baseada em carteira. É como "Fazer login com Google" mas usando sua carteira Solana em vez de um provedor OAuth.
O MWA 2.0 suporta SIWS diretamente no método authorize:
await transact(async (wallet) => {
const authResult = await wallet.authorize({
identity: APP_IDENTITY,
chain: 'solana:mainnet',
sign_in_payload: {
domain: 'mydapp.com',
statement: 'Entrar no Meu dApp',
uri: 'https://mydapp.com',
version: '1',
nonce: generateSecureNonce(), // Gerado pelo servidor
issuedAt: new Date().toISOString(),
},
});
if (authResult.sign_in_result) {
const { address, signature, signedMessage } = authResult.sign_in_result;
// Enviar para o backend para verificação
await verifySignInOnServer(address, signature, signedMessage);
}
});Por que SIWS?
Sem senhas: Usuários autenticam com sua carteira
Prova criptográfica: Impossível de forjar
Descentralizado: Sem dependência de provedor OAuth
Multi-plataforma: A mesma carteira funciona em qualquer lugar
Formato da Mensagem SIWS
O sign_in_payload torna-se uma mensagem legível mostrada ao usuário:
mydapp.com quer que você entre com sua conta Solana:
7F9k...Xyz
Entrar no Meu dApp
URI: https://mydapp.com
Versão: 1
Nonce: abc123
Emitido em: 2024-01-15T10:30:00ZEsta mensagem é o que o usuário assina. A carteira a exibe claramente para que os usuários saibam o que estão aprovando.
Campos do Payload SIWS
type SignInPayload = {
domain: string; // Seu domínio (ex., "mydapp.com")
statement?: string; // Descrição legível
uri: string; // URI completa (ex., "https://mydapp.com")
version: string; // Versão SIWS, atualmente "1"
nonce: string; // String aleatória gerada pelo servidor
issuedAt: string; // Timestamp ISO 8601
expirationTime?: string; // Quando este login expira
notBefore?: string; // Não aceitar antes deste horário
requestId?: string; // Seu identificador de sessão
resources?: string[]; // URIs que o usuário concorda em acessar
};O nonce é crítico; previne ataques de replay. Seu servidor gera um nonce único, o usuário o assina, e seu servidor verifica que a assinatura inclui exatamente esse nonce.
Verificação no Backend
Nunca confie em verificação do lado do cliente para autenticação. Sempre verifique assinaturas SIWS no seu backend.
Exemplo Node.js
import { verifySignIn } from '@solana/wallet-standard-util';
import type { SolanaSignInInput, SolanaSignInOutput } from '@solana/wallet-standard-features';
export function verifySIWS(
input: SolanaSignInInput,
output: SolanaSignInOutput
): boolean {
// Converter para o formato esperado por verifySignIn
const serializedOutput = {
account: {
publicKey: new Uint8Array(output.account.publicKey),
...output.account,
},
signature: new Uint8Array(output.signature),
signedMessage: new Uint8Array(output.signedMessage),
};
return verifySignIn(input, serializedOutput);
}
// Exemplo de rota Express
app.post('/api/auth/siws', async (req, res) => {
const { input, output } = req.body;
// Verificar se o nonce corresponde ao que emitimos
const expectedNonce = await redis.get(`siws:nonce:${req.sessionID}`);
if (input.nonce !== expectedNonce) {
return res.status(400).json({ error: 'Nonce inválido' });
}
// Verificar a assinatura
const isValid = verifySIWS(input, output);
if (!isValid) {
return res.status(401).json({ error: 'Assinatura inválida' });
}
// Assinatura válida - criar sessão
const publicKey = output.account.address;
req.session.wallet = publicKey;
// Limpar o nonce usado
await redis.del(`siws:nonce:${req.sessionID}`);
res.json({ success: true, publicKey });
});Geração de Nonce
import crypto from 'crypto';
export function generateSecureNonce(): string {
return crypto.randomBytes(32).toString('base64url');
}
// Armazenar nonce antes de enviar ao cliente
app.get('/api/auth/nonce', async (req, res) => {
const nonce = generateSecureNonce();
await redis.setex(`siws:nonce:${req.sessionID}`, 300, nonce); // 5 min expiração
res.json({ nonce });
});SIWS Sem Suporte da Carteira
Se uma carteira não suporta o parâmetro sign_in_payload, você pode implementar SIWS manualmente usando signMessages:
import { createSignInMessage, SolanaSignInInput } from '@solana/wallet-standard-util';
async function manualSignIn(): Promise<{ input: SolanaSignInInput; output: any }> {
// Buscar nonce do servidor
const { nonce } = await fetch('/api/auth/nonce').then(r => r.json());
const input: SolanaSignInInput = {
domain: 'mydapp.com',
statement: 'Entrar no Meu dApp',
uri: 'https://mydapp.com',
version: '1',
nonce,
issuedAt: new Date().toISOString(),
};
return await transact(async (wallet) => {
const authResult = await wallet.authorize({
identity: APP_IDENTITY,
chain: 'solana:mainnet',
});
const account = authResult.accounts[0];
const publicKey = new PublicKey(toByteArray(account.address));
// Criar a mensagem SIWS padrão
const messageBytes = createSignInMessage({
...input,
address: publicKey.toBase58(),
});
// Assiná-la
const [signature] = await wallet.signMessages({
addresses: [account.address],
payloads: [messageBytes],
});
return {
input,
output: {
account: {
publicKey: publicKey.toBytes(),
address: publicKey.toBase58(),
},
signature,
signedMessage: messageBytes,
},
};
});
}Casos de Uso
Autenticação
Substitua usuário/senha por assinaturas de carteira:
async function login(): Promise<void> {
const { input, output } = await signInWithSolana();
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ input, output }),
});
if (response.ok) {
const { token } = await response.json();
await AsyncStorage.setItem('auth_token', token);
}
}Atestados Off-Chain
Prove concordância com termos de serviço sem uma transação:
const message = `Eu concordo com os Termos de Serviço do MyApp.
Hash do Documento: ${tosHash}
Timestamp: ${Date.now()}`;
const signature = await signMessage(message);
// Armazenar assinatura como prova de concordância
await fetch('/api/tos/agree', {
method: 'POST',
body: JSON.stringify({
wallet: publicKey.toBase58(),
signature: Buffer.from(signature).toString('base64'),
message,
}),
});Acesso a Conteúdo Restrito
Prove propriedade de NFT sem revelar todas as posses da carteira:
const message = `Verificar propriedade de NFT para acesso:
Coleção: ${collectionAddress}
Timestamp: ${Date.now()}`;
const signature = await signMessage(message);
// Backend verifica se a carteira possui um NFT da coleção
const { hasAccess } = await fetch('/api/verify-access', {
method: 'POST',
body: JSON.stringify({
wallet: publicKey.toBase58(),
signature: Buffer.from(signature).toString('base64'),
message,
}),
}).then(r => r.json());Considerações de Segurança
Sempre Use Nonces
Sem um nonce, assinaturas podem ser reproduzidas:
Usuário entra no seu app
Atacante captura a assinatura
Atacante a reproduz para se passar pelo usuário
Nonces tornam cada assinatura única e de uso único.
Vinculação de Domínio
O campo domain do SIWS deve corresponder ao seu domínio real. Carteiras podem alertar usuários se o domínio parecer suspeito.
Clareza da Mensagem
Usuários veem a mensagem antes de assinar. Torne claro o que estão aprovando:
// Ruim: vago, poderia ser qualquer coisa
const message = 'confirmar ação';
// Bom: específico, inclui contexto
const message = `Entrar no MyApp
ID da Solicitação: ${requestId}
Isso não concede acesso aos seus fundos.`;Validação de Timestamp
Inclua e valide timestamps para evitar que assinaturas antigas sejam reutilizadas:
function isSignatureExpired(issuedAt: string, maxAgeMs: number = 300000): boolean {
const issued = new Date(issuedAt).getTime();
return Date.now() - issued > maxAgeMs;
}A assinatura de mensagens é a base da autenticação baseada em carteira. Na próxima lição, construiremos um AuthorizationProvider para gerenciar todo este estado em sua aplicação.