Overview

This guide shows you how to verify message signatures to authenticate users and ensure data integrity. Message verification is essential for secure dApp interactions and user authentication.

Prerequisites

Step 1: Basic Message Verification

Verify a simple message signature:
import { authenticatedEvmClient } from './client';

const evmClient = await authenticatedEvmClient();

const isValid = await evmClient.verifyMessageSignature({
  accountAddress: '0xYourWalletAddress',
  message: 'Hello, World!',
  signature: '0xYourSignature',
});

console.log('Signature valid:', isValid);

Step 2: Authentication with Nonce

Verify a message with a nonce to prevent replay attacks:
export const verifyAuthentication = async ({
  walletAddress,
  message,
  signature,
  expectedNonce,
}: {
  walletAddress: string;
  message: string;
  signature: string;
  expectedNonce: string;
}) => {
  const evmClient = await authenticatedEvmClient();

  // Extract nonce from message
  const nonceMatch = message.match(/nonce: (\d+)/);
  if (!nonceMatch || nonceMatch[1] !== expectedNonce) {
    throw new Error('Invalid or expired nonce');
  }

  // Verify signature
  const isValid = await evmClient.verifyMessageSignature({
    accountAddress: walletAddress,
    message,
    signature,
  });

  return isValid;
};

// Usage
const isValid = await verifyAuthentication({
  walletAddress: '0xYourWalletAddress',
  message: 'Sign this message to authenticate: nonce: 1234567890',
  signature: '0xYourSignature',
  expectedNonce: '1234567890',
});

if (isValid) {
  console.log('User authenticated successfully');
} else {
  console.log('Authentication failed');
}

Step 3: Data Integrity Verification

Verify that data hasn’t been tampered with:
export const verifyDataIntegrity = async ({
  walletAddress,
  originalData,
  signature,
}: {
  walletAddress: string;
  originalData: any;
  signature: string;
}) => {
  const evmClient = await authenticatedEvmClient();

  // Convert data to string (must match what was signed)
  const message = JSON.stringify(originalData);

  const isValid = await evmClient.verifyMessageSignature({
    accountAddress: walletAddress,
    message,
    signature,
  });

  return isValid;
};

// Usage
const data = { userId: 123, action: 'transfer', amount: '100' };
const isValid = await verifyDataIntegrity({
  walletAddress: '0xYourWalletAddress',
  originalData: data,
  signature: '0xYourSignature',
});

if (isValid) {
  console.log('Data integrity verified');
} else {
  console.log('Data has been tampered with');
}

Step 5: Complete Authentication Flow

Here’s a complete example of a secure authentication flow:
export class MessageVerifier {
  private evmClient: any;
  private nonceStore: Map<string, number> = new Map();

  constructor() {
    this.evmClient = await authenticatedEvmClient();
  }

  // Generate a nonce for a user
  generateNonce(userId: string): string {
    const nonce = Date.now();
    this.nonceStore.set(userId, nonce);
    return nonce.toString();
  }

  // Verify user authentication
  async verifyUser({
    walletAddress,
    userId,
    signature,
  }: {
    walletAddress: string;
    userId: string;
    signature: string;
  }): Promise<boolean> {
    const expectedNonce = this.nonceStore.get(userId);
    if (!expectedNonce) {
      throw new Error('No nonce found for user');
    }

    const message = `Sign this message to authenticate user ${userId}: nonce: ${expectedNonce}`;

    const isValid = await this.evmClient.verifyMessageSignature({
      accountAddress: walletAddress,
      message,
      signature,
    });

    if (isValid) {
      // Clear the used nonce
      this.nonceStore.delete(userId);
    }

    return isValid;
  }

  // Verify transaction approval
  async verifyTransactionApproval({
    walletAddress,
    transactionHash,
    signature,
  }: {
    walletAddress: string;
    transactionHash: string;
    signature: string;
  }): Promise<boolean> {
    const message = `I approve transaction: ${transactionHash}`;

    return await this.evmClient.verifyMessageSignature({
      accountAddress: walletAddress,
      message,
      signature,
    });
  }
}

// Usage
const verifier = new MessageVerifier();

// Step 1: Generate nonce for user
const nonce = verifier.generateNonce('user123');
console.log('Nonce generated:', nonce);

// Step 2: User signs message with nonce (on frontend)
// const message = `Sign this message to authenticate user user123: nonce: ${nonce}`;
// const signature = await wallet.signMessage(message);

// Step 3: Verify signature
const isValid = await verifier.verifyUser({
  walletAddress: '0xUserWalletAddress',
  userId: 'user123',
  signature: '0xUserSignature',
});

if (isValid) {
  console.log('User authenticated successfully');
} else {
  console.log('Authentication failed');
}

Best Practices

  1. Nonce Usage: Always use nonces to prevent replay attacks
  2. Message Format: Use consistent message formats across your application
  3. Error Handling: Implement proper error handling for verification failures
  4. Security: Never trust client-side verification - always verify on the server
  5. Nonce Management: Clear used nonces to prevent reuse

Common Use Cases

  • User Authentication: Verify wallet ownership for login
  • Transaction Approval: Verify user approval for transactions
  • Data Integrity: Ensure data hasn’t been modified
  • Access Control: Verify permissions for specific actions

Next Steps