此内容正在翻译中,完成后将会在此处提供。
Signing Messages

Not every interaction needs a transaction. Sometimes you just need to prove that a user controls a wallet, without writing anything to the blockchain or spending lamports.
Message signing enables authentication flows, off-chain attestations, and cryptographic proofs of ownership. Sign In With Solana (SIWS) standardizes this for web3 login, replacing email/password with wallet signatures.
In this lesson, you'll create a useSignMessage hook and learn how to implement Sign In With Solana for authentication.
How Message Signing Works
When you sign a transaction, you're authorizing a state change on Solana. When you sign a message, you're creating a cryptographic proof that says "this wallet approved this exact content."
The signature is deterministic: the same wallet signing the same message will always produce the same signature. Anyone with the public key can verify the signature was made by the corresponding private key, without ever seeing that private key.
Message + Private Key → Signature
Message + Signature + Public Key → True/False (verification)This is Ed25519 cryptography, the same scheme Solana uses for transaction signatures.
Basic Message Signing
The signMessages method takes an array of payloads (byte arrays) and returns signatures:
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',
});
// Convert string to bytes
const messageBytes = new TextEncoder().encode(message);
// Sign the message
const signatures = await wallet.signMessages({
addresses: [authResult.accounts[0].address],
payloads: [messageBytes],
});
// signatures[0] is Uint8Array (64 bytes)
return signatures[0];
});
}Key points:
addresses: Which account(s) should signpayloads: The raw bytes to signReturns: Array of signatures matching the payloads
The wallet shows users the message content (decoded as UTF-8 if possible) and asks for approval.
Signing Multiple Messages
You can sign multiple messages in one request:
const signatures = await wallet.signMessages({
addresses: [account.address, account.address],
payloads: [messageBytes1, messageBytes2],
});
// signatures[0] corresponds to messageBytes1
// signatures[1] corresponds to messageBytes2Verifying Signatures
After receiving a signature, you (or your backend) can verify it:
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()
);
}
// Usage
const message = new TextEncoder().encode('Hello, Solana!');
const signature = await signMessage('Hello, Solana!');
const publicKey = /* user's public key from authorization */;
const isValid = verifySignature(message, signature, publicKey);
console.log('Signature valid:', isValid); // trueImportant: You must verify against the exact same bytes that were signed. If you encode the message differently (different encoding, whitespace, newlines), verification will fail.
Sign In With Solana
Sign In With Solana (SIWS) is a standard for wallet-based authentication. It's like "Sign in with Google" but using your Solana wallet instead of an OAuth provider.
MWA 2.0 supports SIWS directly in the authorize method:
await transact(async (wallet) => {
const authResult = await wallet.authorize({
identity: APP_IDENTITY,
chain: 'solana:mainnet',
sign_in_payload: {
domain: 'mydapp.com',
statement: 'Sign in to My dApp',
uri: 'https://mydapp.com',
version: '1',
nonce: generateSecureNonce(), // Server-generated
issuedAt: new Date().toISOString(),
},
});
if (authResult.sign_in_result) {
const { address, signature, signedMessage } = authResult.sign_in_result;
// Send to backend for verification
await verifySignInOnServer(address, signature, signedMessage);
}
});Why SIWS?
No passwords: Users authenticate with their wallet
Cryptographic proof: Impossible to forge
Decentralized: No OAuth provider dependency
Cross-platform: Same wallet works everywhere
The SIWS Message Format
The sign_in_payload becomes a human-readable message shown to the user:
mydapp.com wants you to sign in with your Solana account:
7F9k...Xyz
Sign in to My dApp
URI: https://mydapp.com
Version: 1
Nonce: abc123
Issued At: 2024-01-15T10:30:00ZThis message is what the user signs. The wallet displays it clearly so users know what they're approving.
SIWS Payload Fields
type SignInPayload = {
domain: string; // Your domain (e.g., "mydapp.com")
statement?: string; // Human-readable description
uri: string; // Full URI (e.g., "https://mydapp.com")
version: string; // SIWS version, currently "1"
nonce: string; // Server-generated random string
issuedAt: string; // ISO 8601 timestamp
expirationTime?: string; // When this sign-in expires
notBefore?: string; // Don't accept before this time
requestId?: string; // Your session identifier
resources?: string[]; // URIs the user is agreeing to access
};The nonce is critical; it prevents replay attacks. Your server generates a unique nonce, the user signs it, and your server verifies the signature includes that exact nonce.
Backend Verification
Never trust client-side verification for authentication. Always verify SIWS signatures on your backend.
Node.js Example
import { verifySignIn } from '@solana/wallet-standard-util';
import type { SolanaSignInInput, SolanaSignInOutput } from '@solana/wallet-standard-features';
export function verifySIWS(
input: SolanaSignInInput,
output: SolanaSignInOutput
): boolean {
// Convert to format expected by 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);
}
// Express route example
app.post('/api/auth/siws', async (req, res) => {
const { input, output } = req.body;
// Verify the nonce matches what we issued
const expectedNonce = await redis.get(`siws:nonce:${req.sessionID}`);
if (input.nonce !== expectedNonce) {
return res.status(400).json({ error: 'Invalid nonce' });
}
// Verify the signature
const isValid = verifySIWS(input, output);
if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Signature valid - create session
const publicKey = output.account.address;
req.session.wallet = publicKey;
// Clear the used nonce
await redis.del(`siws:nonce:${req.sessionID}`);
res.json({ success: true, publicKey });
});Nonce Generation
import crypto from 'crypto';
export function generateSecureNonce(): string {
return crypto.randomBytes(32).toString('base64url');
}
// Store nonce before sending to client
app.get('/api/auth/nonce', async (req, res) => {
const nonce = generateSecureNonce();
await redis.setex(`siws:nonce:${req.sessionID}`, 300, nonce); // 5 min expiry
res.json({ nonce });
});SIWS Without Wallet Support
If a wallet doesn't support the sign_in_payload parameter, you can manually implement SIWS using signMessages:
import { createSignInMessage, SolanaSignInInput } from '@solana/wallet-standard-util';
async function manualSignIn(): Promise<{ input: SolanaSignInInput; output: any }> {
// Fetch nonce from server
const { nonce } = await fetch('/api/auth/nonce').then(r => r.json());
const input: SolanaSignInInput = {
domain: 'mydapp.com',
statement: 'Sign in to My 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));
// Create the standard SIWS message
const messageBytes = createSignInMessage({
...input,
address: publicKey.toBase58(),
});
// Sign it
const [signature] = await wallet.signMessages({
addresses: [account.address],
payloads: [messageBytes],
});
return {
input,
output: {
account: {
publicKey: publicKey.toBytes(),
address: publicKey.toBase58(),
},
signature,
signedMessage: messageBytes,
},
};
});
}Use Cases
Authentication
Replace username/password with wallet signatures:
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);
}
}Off-Chain Attestations
Prove agreement to terms of service without a transaction:
const message = `I agree to the Terms of Service for MyApp.
Document Hash: ${tosHash}
Timestamp: ${Date.now()}`;
const signature = await signMessage(message);
// Store signature as proof of agreement
await fetch('/api/tos/agree', {
method: 'POST',
body: JSON.stringify({
wallet: publicKey.toBase58(),
signature: Buffer.from(signature).toString('base64'),
message,
}),
});Gated Content Access
Prove NFT ownership without revealing the wallet's full holdings:
const message = `Verify NFT ownership for access:
Collection: ${collectionAddress}
Timestamp: ${Date.now()}`;
const signature = await signMessage(message);
// Backend verifies wallet holds an NFT from the collection
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());Security Considerations
Always Use Nonces
Without a nonce, signatures can be replayed:
User signs in to your app
Attacker captures the signature
Attacker replays it to impersonate the user
Nonces make each signature unique and single-use.
Domain Binding
The SIWS domain field should match your actual domain. Wallets may warn users if the domain looks suspicious.
Message Clarity
Users see the message before signing. Make it clear what they're approving:
// Bad: vague, could be anything
const message = 'confirm action';
// Good: specific, includes context
const message = `Sign in to MyApp
Request ID: ${requestId}
This does not grant access to your funds.`;Timestamp Validation
Include and validate timestamps to prevent old signatures from being reused:
function isSignatureExpired(issuedAt: string, maxAgeMs: number = 300000): boolean {
const issued = new Date(issuedAt).getTime();
return Date.now() - issued > maxAgeMs;
}Message signing is the foundation of wallet-based authentication. In the next lesson, we'll build an AuthorizationProvider to manage all this state across your application.