Firmare messaggi e autenticare gli utenti

La firma di 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:

  • Messaggi off-chain (solana:signOffchainMessage) racchiudono il tuo testo in un formato standard, definito dalla specifica della 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 firmato dagli hardware wallet.
  • Messaggi raw (solana:signMessage) firmano esattamente i byte passati dalla tua app, senza prefisso e senza firmatari dichiarati. La maggior parte dei software wallet li supporta, ma nulla nei byte li identifica come messaggio anziché come transazione.

Usa i messaggi off-chain quando il wallet li supporta e ricorri ai messaggi raw quando non li supporta. Il resto di questa guida usa l'accesso come esempio pratico, ma gli stessi passaggi di firma e verifica si applicano a qualsiasi messaggio.

Scegliere un formato di messaggio

Messaggio off-chainMessaggio rawTransazione
Funzionalità del walletsignOffchainMessagesignMessagesignTransaction
Supporto 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 sulla richiesta a questi utenti di firmare invece 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 esegue il polling per la conferma.
  • Latenza aggiuntiva. Ognuna di queste chiamate è un round trip prima che l'utente abbia effettuato l'accesso.
  • Nulla di leggibile. L'utente approva i byte della 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 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.

Che 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 quindi 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 copre 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 un fallback 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 la stessa challenge con solana:signMessage. La maggior parte dei software wallet la supporta.
  3. Offri agli utenti di hardware wallet senza supporto per i messaggi un consenso esplicito, ad esempio un toggle "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 firmati dal wallet, e signature. Codifica entrambi in Base64 e inviali al tuo server con l'indirizzo del wallet.

Scrivi la challenge in modo che si legga bene su uno schermo piccolo. Indica il nome della tua app e il dominio, spiega che cosa comporta la firma e includi un nonce monouso e un'ora di emissione provenienti 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 server

La verifica è un controllo locale della firma e non richiede alcuna connessione RPC. Decodifica i byte firmati dal wallet, conferma che corrispondano alla challenge che hai 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'eccezione se il contenuto o i firmatari differiscono da quanto ti aspettavi. verifyOffchainMessageEnvelope genera un'eccezione se la firma è mancante o non valida. Cerca expectedMessage per nonce sul tuo server invece di fidarti della copia restituita dal client. Consuma il nonce una volta verificato, così la stessa firma non può effettuare l'accesso due volte.

Verificare un messaggio raw

Una firma signMessage copre direttamente i byte della challenge, quindi il client deve inviare solo la firma e l'indirizzo del wallet. Controllala rispetto alla challenge che hai 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");
}
}

Si applicano le stesse regole per il nonce: cerca expectedMessage per nonce e consumalo una volta verificato.

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

Sign In With Solana

Sign In With Solana (SIWS) standardizza la challenge stessa. 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 un'opzione useOffchainMessage. Quando passi useOffchainMessage: { messageVersion: 1 }, il wallet racchiude il testo SIWS in un messaggio off-chain versione 1 con l'account di firma come unico firmatario richiesto. Gli hardware wallet possono visualizzarlo 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 altrimenti omettila.

solana:signIn è una funzionalità del wallet, non una funzionalità 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 dal 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 segnala 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 di firma 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 copre 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 dei messaggi che puoi firmare in supportedMessageVersions. Oggi è [1].
  • Costruisci 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.