Firma mensajes y autentica usuarios

Firmar un mensaje demuestra que un usuario controla una billetera, sin transacción, sin comisión y sin llamadas a la red. Las aplicaciones usan mensajes firmados para iniciar sesión, registrar que un usuario aceptó los términos, vincular una billetera a una cuenta existente o autorizar una acción que ocurre fuera de la cadena, como realizar un pedido.

Las billeteras de Solana firman mensajes de dos maneras:

  • Mensajes fuera de la cadena (solana:signOffchainMessage) envuelven tu texto en un formato estándar, definido por la especificación de firma de mensajes fuera de la cadena (OCMS). El formato nunca puede confundirse con una transacción, indica las cuentas que deben firmarlo y es el formato que firman las billeteras de hardware.
  • Mensajes sin procesar (solana:signMessage) firman exactamente los bytes que tu aplicación les pasa, sin prefijo y sin firmantes declarados. La mayoría de las billeteras de software los admiten, pero nada en los bytes los marca como un mensaje en lugar de una transacción.

Usa mensajes fuera de la cadena cuando la billetera los admita y recurre a los mensajes sin procesar cuando no. En el resto de esta guía se usa el inicio de sesión como ejemplo práctico, pero los mismos pasos de firma y verificación se aplican a cualquier mensaje.

Elige un formato de mensaje

Mensaje fuera de la cadenaMensaje sin procesarTransacción
Función de la billeterasignOffchainMessagesignMessagesignTransaction
Compatibilidad con billeteras de hardwareDepende del dispositivoNoSí
Legible para el usuarioSíSolo en softwareDepende del dispositivo
Declara los firmantes requeridosSíNoSí
Llamadas RPC y comisionesNingunaNingunaBlockhash, envío, comisión, confirmación

¿Por qué no una transacción?

Antes, las billeteras de hardware no tenían forma de firmar un mensaje legible. Muchas aplicaciones optaban por pedir a estos usuarios que firmaran una transacción sin efecto. Funciona, pero añade latencia, costo y riesgo:

  • Comisiones de red. Una transacción paga una comisión, incluso cuando no hace nada.
  • Múltiples llamadas RPC. Tu aplicación obtiene un blockhash, simula, envía y luego consulta repetidamente la confirmación.
  • Latencia adicional. Cada una de esas llamadas es un viaje de ida y vuelta antes de que el usuario inicie sesión.
  • Nada legible. El usuario aprueba los bytes de una transacción, no una declaración de aquello con lo que está de acuerdo.
  • Riesgo de comportamiento. Pedir a los usuarios que firmen una transacción solo para iniciar sesión facilita engañarlos para que firmen una dañina.

Un mensaje fuera de la cadena evita todo eso. El único viaje de ida y vuelta es hacia tu propio servidor.

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.

Qué firma la billetera

Un mensaje fuera de la cadena comienza con un dominio de firma fijo, \xffsolana offchain. Ninguna transacción válida empieza con esos bytes, por lo que un mensaje firmado nunca puede usarse como transacción. La versión 1 del formato enumera a continuación los firmantes requeridos y transporta el mensaje como texto UTF-8, sin límite de longitud.

La billetera construye estos bytes, los firma y los devuelve junto con la firma. La especificación cubre el formato completo, incluida la versión 0 heredada.

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.

Elige un método de inicio de sesión

Aún no todas las billeteras admiten mensajes fuera de la cadena. Comprueba las funciones de la cuenta conectada antes de solicitar una firma y recurre a la alternativa cuando falte la función:

  1. Si la cuenta admite solana:signOffchainMessage con la versión 1 del mensaje, firma un mensaje fuera de la cadena.
  2. De lo contrario, firma el mismo desafío con solana:signMessage. La mayoría de las billeteras de software lo admiten.
  3. Ofrece a los usuarios de billeteras de hardware sin compatibilidad con mensajes una opción explícita, como un interruptor «Estoy usando una billetera de hardware», que firme en su lugar una transacción de inicio de sesión. No los dirijas por este camino de forma predeterminada.
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.

Firma un mensaje en el navegador

Instala los paquetes de Wallet Standard junto con Kit:

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

Estas utilidades reciben un UiWalletAccount, como el account que devuelve useConnectedWallet en la guía de 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 devuelve signedOffchainMessage, los bytes completos que firmó la billetera, y signature. Codifica ambos en Base64 y envíalos a tu servidor junto con la dirección de la billetera.

Redacta el desafío de modo que se lea bien en una pantalla pequeña. Indica el nombre de tu aplicación y su dominio, explica qué hace la firma e incluye un nonce de un solo uso y una hora de emisión generada por tu servidor:

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

Verifica la firma en tu servidor

La verificación es una comprobación local de la firma y no necesita conexión RPC. Decodifica los bytes que firmó la billetera, confirma que coinciden con el desafío que emitiste y luego comprueba 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 lanza un error si el contenido o los firmantes difieren de lo que esperabas. verifyOffchainMessageEnvelope lanza un error si la firma falta o no es válida. Busca expectedMessage por nonce en tu servidor en lugar de confiar en la copia que devuelve el cliente. Consume el nonce una vez verificado, para que la misma firma no pueda iniciar sesión dos veces.

Verifica un mensaje sin procesar

Una firma de signMessage cubre directamente los bytes del desafío, así que el cliente solo necesita enviar la firma y la dirección de la billetera. Comprúebala frente al desafío que emitiste:

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");
}
}

Se aplican las mismas reglas de nonce: busca expectedMessage por nonce y consúmelo una vez verificado.

@solana/kit reexporta estas utilidades desde @solana/offchain-messages. Usa el paquete independiente si tu servidor no depende de Kit.

Sign In With Solana

Sign In With Solana (SIWS) estandariza el propio desafío. Tu aplicación pasa campos estructurados, como domain, statement, nonce e issuedAt, a la función solana:signIn de la billetera. La billetera los formatea en un mensaje estándar, se conecta si es necesario y lo firma en un solo paso.

La versión 1.1.0 de solana:signIn añade la opción useOffchainMessage. Cuando pasas useOffchainMessage: { messageVersion: 1 }, la billetera envuelve el texto SIWS en un mensaje fuera de la cadena de la versión 1, con la cuenta que firma como único firmante requerido. Las billeteras de hardware pueden mostrarlo y firmarlo, y el resultado informa signedMessageFormat: { kind: "offchainMessage", messageVersion: 1 }. Las billeteras que anuncian la versión 1.0.0 no conocen la opción, así que comprueba primero la versión de la función y omítela en caso contrario.

solana:signIn es una función de la billetera, no de la cuenta, por lo que estas utilidades reciben 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;
}

Construye la entrada en tu servidor, con un nonce de un solo uso y una hora issuedAt, y conserva una copia indexada por el nonce. Consume el nonce una vez que el inicio de sesión se verifique.

Sin Wallet Adapter, verifySignIn de @solana/wallet-standard-util comprueba solo la salida SIWS simple. Cuando signedMessageFormat indica un mensaje fuera de la cadena, signedMessage es un mensaje fuera de la cadena cuyo contenido es el texto SIWS. Reconstruye el texto esperado a partir de la entrada almacenada y la dirección firmante con createSignInMessageText de @solana/wallet-standard-util y luego verifícalo como cualquier otro mensaje fuera de la cadena, como en Verifica la firma en tu servidor. El README de @solana/offchain-messages cubre este flujo en Kit.

Para billeteras

Las billeteras añaden compatibilidad implementando la función de Wallet Standard solana:signOffchainMessage de @solana/wallet-standard-features:

  • Informa version: "1.0.0" y enumera las versiones de mensaje que puedes firmar en supportedMessageVersions. Hoy, eso es [1].
  • Construye el preámbulo de la versión 1 a partir de requiredSigners, que debes ordenar y revisar para detectar duplicados. Rechaza la solicitud si la cuenta no es uno de los firmantes requeridos.
  • Muestra al usuario el texto del mensaje completo antes de firmar, también en dispositivos de hardware.
  • Devuelve los bytes exactos que firmaste como signedOffchainMessage, junto con la signature Ed25519.
  • Para admitir Sign In With Solana fuera de la cadena, indica version: "1.1.0" para solana:signIn. Cuando la entrada incluya useOffchainMessage, firma el texto de SIWS como un mensaje fuera de la cadena de la versión 1, devuelve sus bytes como signedMessage y establece signedMessageFormat.
  • Sigue admitiendo solana:signMessage. No ha cambiado, y las aplicaciones aún lo usan como alternativa.

Is this page helpful?

Tabla de Contenidos

Editar Página