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 apps 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 off-chain, como realizar un pedido.

Las billeteras de Solana firman mensajes de dos maneras:

  • Mensajes off-chain (solana:signOffchainMessage) envuelven tu texto en un formato estándar, definido por la especificación de firma de mensajes off-chain (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 app les pasa, sin prefijo y sin firmantes declarados. La mayoría de las billeteras de software los admiten, pero nada en los bytes los identifica como un mensaje y no como una transacción.

Usa mensajes off-chain cuando la billetera los admita y recurre a los mensajes sin procesar cuando no. El resto de esta guía 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 off-chainMensaje 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 apps recurrían a pedir a estos usuarios que firmaran una transacción sin operación. 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 app obtiene un blockhash, simula, envía y luego consulta periódicamente 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 off-chain evita todo eso. El único viaje de ida y vuelta es a 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 off-chain 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 después 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 off-chain. 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 off-chain.
  2. En caso 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

Estos helpers reciben un UiWalletAccount, como el account devuelto por 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 app y el dominio, explica qué hace la firma e incluye un nonce de un solo uso y una hora de emisión de 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 esperado. 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, por lo 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 estos helpers 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 app pasa campos estructurados, como domain, statement, nonce e issuedAt, a la función solana:signIn de la billetera. La billetera los da formato 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 una opción useOffchainMessage. Cuando pasas useOffchainMessage: { messageVersion: 1 }, la billetera envuelve el texto SIWS en un mensaje off-chain de versión 1 con la cuenta firmante como su único firmante requerido. Las billeteras de hardware pueden mostrarlo y firmarlo, y la salida 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 estos helpers 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 guarda una copia indexada por el nonce. Consume el nonce una vez verificado el inicio de sesión.

Sin Wallet Adapter, verifySignIn de @solana/wallet-standard-util comprueba únicamente la salida SIWS simple. Cuando signedMessageFormat indica un mensaje off-chain, signedMessage es un mensaje off-chain 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 off-chain, 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 comprobar para detectar duplicados. Rechaza la solicitud si la cuenta no es uno de los firmantes requeridos.
  • Muestra el texto del mensaje al usuario por completo antes de firmar, incluso 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