Firmare messaggi e autenticare gli utenti

Firmare un messaggio dimostra che un utente controlla un wallet, senza transazioni, senza commissioni e senza chiamate di rete. Le app usano i messaggi firmati per far accedere gli utenti, registrare che un utente ha accettato i termini, collegare un wallet a un account esistente o autorizzare un'azione che avviene off-chain, come l'invio di un ordine.

I wallet Solana firmano i messaggi in due modi:

  • I messaggi off-chain (solana:signOffchainMessage) racchiudono il tuo testo in un formato standard, definito dalla specifica per la firma di messaggi off-chain (OCMS). Il formato non può mai essere scambiato per una transazione, indica gli account che devono firmarlo ed è il formato che firmano gli hardware wallet.
  • I messaggi raw (solana:signMessage) firmano esattamente i byte che la tua app passa, senza prefisso e senza firmatari dichiarati. La maggior parte dei wallet software li supporta, ma nulla nei byte li contrassegna come messaggio anziché come transazione.

Usa i messaggi off-chain dove il wallet li supporta e ripiega sui messaggi raw dove non li supporta. Il resto di questa guida usa l'accesso come esempio pratico, ma gli stessi passaggi di firma e verifica valgono per qualsiasi messaggio.

Scegliere un formato di messaggio

Messaggio off-chainMessaggio rawTransazione
Funzionalità del walletsignOffchainMessagesignMessagesignTransaction
Supporto per hardware walletDipende dal dispositivoNoSì
Leggibile dall'utenteSìSolo softwareDipende dal dispositivo
Dichiara i firmatari richiestiSìNoSì
Chiamate RPC e commissioniNessunaNessunaBlockhash, invio, commissione, conferma

Perché non una transazione?

In passato gli hardware wallet non avevano modo di firmare un messaggio leggibile. Molte app ripiegavano sul chiedere a questi utenti di firmare una transazione no-op. Funziona, ma aggiunge latenza, costi e rischi:

  • Commissioni di rete. Una transazione paga una commissione, anche quando non fa nulla.
  • Più chiamate RPC. La tua app recupera un blockhash, simula, invia e poi controlla la conferma.
  • Latenza aggiuntiva. Ognuna di queste chiamate è un round trip prima che l'utente acceda.
  • Niente di leggibile. L'utente approva i byte di una transazione, non una dichiarazione di ciò a cui sta acconsentendo.
  • Rischio comportamentale. Chiedere agli utenti di firmare una transazione solo per accedere li rende più facili da ingannare e indurre a firmarne una dannosa.

Un messaggio off-chain evita tutto questo. L'unico round trip è verso il tuo server.

A sign-in transaction fetches a blockhash, signs, simulates, sends, and polls for confirmation before the server can verify it. An off-chain message goes from a server challenge to a wallet signature to a server-side check, with no RPC calls and no fee.

Cosa firma il wallet

Un messaggio off-chain inizia con un dominio di firma fisso, \xffsolana offchain. Nessuna transazione valida inizia con quei byte, quindi un messaggio firmato non può mai essere usato come transazione. La versione 1 del formato elenca poi i firmatari richiesti e contiene il messaggio come testo UTF-8, senza limite di lunghezza.

Il wallet costruisce questi byte, li firma e li restituisce insieme alla firma. La specifica descrive il formato completo, inclusa la versione legacy 0.

A version 1 off-chain message is a 16-byte signing domain, a version byte, a signer count, each signer's 32-byte public key in sorted order, and the UTF-8 content. The envelope prefixes it with a signature count and one 64-byte signature per signer.

Scegliere un metodo di accesso

Non tutti i wallet supportano ancora i messaggi off-chain. Controlla le funzionalità dell'account connesso prima di richiedere una firma e usa l'alternativa quando la funzionalità manca:

  1. Se l'account supporta solana:signOffchainMessage con la versione 1 del messaggio, firma un messaggio off-chain.
  2. Altrimenti, firma lo stesso challenge con solana:signMessage. La maggior parte dei wallet software lo supporta.
  3. Offri agli utenti di hardware wallet senza supporto ai messaggi un'opzione esplicita, ad esempio un interruttore "Sto usando un hardware wallet", che firmi invece una transazione di accesso. Non indirizzarli su questo percorso per impostazione predefinita.
When a wallet connects, check whether its account supports solana:signOffchainMessage version 1. If it does, sign an off-chain message. If not, sign a raw message with solana:signMessage, and let hardware wallet users opt into signing a sign-in transaction.

Firmare un messaggio nel browser

Installa i pacchetti Wallet Standard insieme a Kit:

Terminal
$
npm install @solana/kit @solana/wallet-standard-features @wallet-standard/ui @wallet-standard/ui-features @wallet-standard/ui-registry

Questi helper accettano un UiWalletAccount, come l'account restituito da useConnectedWallet nella guida React:

offchain-message.ts
import {
SolanaSignOffchainMessage,
type SolanaSignOffchainMessageFeature
} from "@solana/wallet-standard-features";
import type { UiWalletAccount } from "@wallet-standard/ui";
import { getWalletAccountFeature } from "@wallet-standard/ui-features";
import { getWalletAccountForUiWalletAccount } from "@wallet-standard/ui-registry";
type OffchainMessageFeature =
SolanaSignOffchainMessageFeature[typeof SolanaSignOffchainMessage];
export function supportsOffchainMessages(account: UiWalletAccount) {
if (!account.features.includes(SolanaSignOffchainMessage)) return false;
const feature = getWalletAccountFeature(
account,
SolanaSignOffchainMessage
) as OffchainMessageFeature;
return feature.supportedMessageVersions.includes(1);
}
export async function signOffchainMessage(
account: UiWalletAccount,
message: string
) {
const feature = getWalletAccountFeature(
account,
SolanaSignOffchainMessage
) as OffchainMessageFeature;
const [output] = await feature.signOffchainMessage({
messageVersion: 1,
account: getWalletAccountForUiWalletAccount(account),
message,
requiredSigners: [account.publicKey]
});
return output;
}

signOffchainMessage restituisce signedOffchainMessage, i byte completi che il wallet ha firmato, e signature. Codifica entrambi in Base64 e inviali al tuo server insieme all'indirizzo del wallet.

Scrivi il challenge in modo che si legga bene su uno schermo piccolo. Indica il nome della tua app e il dominio, spiega cosa comporta la firma e includi un nonce monouso e un orario di emissione dal tuo server:

swap.app wants you to sign in with your Solana account.
Signing in costs nothing and does not submit a transaction.
Nonce: 8f3a1c9e
Issued at: 2026-09-29T17:04:00Z

Verificare la firma sul tuo server

La verifica è un controllo locale della firma e non richiede alcuna connessione RPC. Decodifica i byte firmati dal wallet, conferma che corrispondano al challenge emesso, quindi controlla la firma:

verify-sign-in.ts
import {
address,
assertOffchainMessageV1Equal,
getBase64Encoder,
getOffchainMessageDecoder,
verifyOffchainMessageEnvelope,
type OffchainMessageBytes,
type SignatureBytes
} from "@solana/kit";
export async function verifySignIn(input: {
walletAddress: string;
expectedMessage: string;
signedOffchainMessage: string;
signature: string;
}) {
const signer = address(input.walletAddress);
const base64 = getBase64Encoder();
const content = base64.encode(
input.signedOffchainMessage
) as OffchainMessageBytes;
const signature = base64.encode(input.signature) as SignatureBytes;
const received = getOffchainMessageDecoder().decode(content);
if (received.version !== 1) {
throw new Error(`Expected a v1 message, got v${received.version}`);
}
assertOffchainMessageV1Equal(received, {
version: 1,
requiredSignatories: [{ address: signer }],
content: input.expectedMessage
});
await verifyOffchainMessageEnvelope({
content,
signatures: { [signer]: signature }
});
}

assertOffchainMessageV1Equal genera un errore se il contenuto o i firmatari differiscono da quelli attesi. verifyOffchainMessageEnvelope genera un errore se la firma è assente o non valida. Cerca expectedMessage tramite nonce sul tuo server anziché fidarti della copia che il client rimanda indietro. Consuma il nonce una volta verificato, in modo che la stessa firma non possa servire per accedere due volte.

Verificare un messaggio raw

Una firma signMessage copre direttamente i byte del challenge, quindi il client deve inviare solo la firma e l'indirizzo del wallet. Verificala rispetto al challenge emesso:

verify-raw-sign-in.ts
import {
address,
getBase64Encoder,
getPublicKeyFromAddress,
getUtf8Encoder,
signatureBytes,
verifySignature
} from "@solana/kit";
export async function verifyRawSignIn(input: {
walletAddress: string;
expectedMessage: string;
signature: string;
}) {
const publicKey = await getPublicKeyFromAddress(address(input.walletAddress));
const signature = signatureBytes(getBase64Encoder().encode(input.signature));
const message = getUtf8Encoder().encode(input.expectedMessage);
if (!(await verifySignature(publicKey, signature, message))) {
throw new Error("Invalid signature");
}
}

Valgono le stesse regole sul nonce: cerca expectedMessage tramite nonce e consumalo una volta verificato.

@solana/kit riesporta questi helper da @solana/offchain-messages. Usa il pacchetto autonomo se il tuo server non dipende da Kit.

Sign In With Solana

Sign In With Solana (SIWS) standardizza il challenge stesso. La tua app passa campi strutturati, come domain, statement, nonce e issuedAt, alla funzionalità solana:signIn del wallet. Il wallet li formatta in un messaggio standard, si connette se necessario e lo firma in un unico passaggio.

La versione 1.1.0 di solana:signIn aggiunge l'opzione useOffchainMessage. Quando passi useOffchainMessage: { messageVersion: 1 }, il wallet racchiude il testo SIWS in un messaggio off-chain di versione 1 con l'account firmatario come unico firmatario richiesto. Gli hardware wallet possono mostrarlo e firmarlo, e l'output riporta signedMessageFormat: { kind: "offchainMessage", messageVersion: 1 }. I wallet che dichiarano la versione 1.0.0 non conoscono l'opzione, quindi controlla prima la versione della funzionalità e omettila in caso contrario.

solana:signIn è una funzionalità del wallet, non dell'account, quindi questi helper accettano un UiWallet:

sign-in.ts
import {
SolanaSignIn,
type SolanaSignInFeature,
type SolanaSignInInput
} from "@solana/wallet-standard-features";
import type { UiWallet } from "@wallet-standard/ui";
import { getWalletFeature } from "@wallet-standard/ui-features";
type SignInFeature = SolanaSignInFeature[typeof SolanaSignIn];
export function supportsOffchainSignIn(wallet: UiWallet) {
if (!wallet.features.includes(SolanaSignIn)) return false;
const feature = getWalletFeature(wallet, SolanaSignIn) as SignInFeature;
return feature.version === "1.1.0";
}
export async function signIn(wallet: UiWallet, input: SolanaSignInInput) {
const feature = getWalletFeature(wallet, SolanaSignIn) as SignInFeature;
const [output] = await feature.signIn({
...input,
...(supportsOffchainSignIn(wallet) && {
useOffchainMessage: { messageVersion: 1 }
})
});
return output;
}

Costruisci l'input sul tuo server, con un nonce monouso e un orario issuedAt, e conserva una copia indicizzata per nonce. Consuma il nonce una volta verificato l'accesso.

Senza Wallet Adapter, verifySignIn da @solana/wallet-standard-util controlla solo l'output SIWS semplice. Quando signedMessageFormat indica un messaggio off-chain, signedMessage è un messaggio off-chain il cui contenuto è il testo SIWS. Ricostruisci il testo atteso dall'input memorizzato e dall'indirizzo firmatario con createSignInMessageText da @solana/wallet-standard-util, quindi verificalo come qualsiasi altro messaggio off-chain, come in Verificare la firma sul tuo server. Il README di @solana/offchain-messages descrive questo flusso in Kit.

Per i wallet

I wallet aggiungono il supporto implementando la funzionalità Wallet Standard solana:signOffchainMessage da @solana/wallet-standard-features:

  • Riporta version: "1.0.0" ed elenca le versioni di messaggio che puoi firmare in supportedMessageVersions. Oggi, è [1].
  • Crea il preambolo della versione 1 a partire da requiredSigners, che devi ordinare e controllare per individuare eventuali duplicati. Rifiuta la richiesta se l'account non è uno dei firmatari richiesti.
  • Mostra all'utente il testo del messaggio per intero prima della firma, anche sui dispositivi hardware.
  • Restituisci i byte esatti che hai firmato come signedOffchainMessage, insieme alla signature Ed25519.
  • Per supportare Sign In With Solana off-chain, indica version: "1.1.0" per solana:signIn. Quando l'input include useOffchainMessage, firma il testo SIWS come messaggio off-chain di versione 1, restituisci i relativi byte come signedMessage e imposta signedMessageFormat.
  • Continua a supportare solana:signMessage. Non è cambiato e le app lo usano ancora come soluzione di ripiego.

Is this page helpful?

Indice dei contenuti

Modifica pagina
© 2026 Solana Foundation. Tutti i diritti riservati.