Skip to main content

Basic Message Signing

import { useWallet } from '@hermis/solana-headless-react';

function SignMessage() {
  const { signMessage, publicKey } = useWallet();

  const handleSign = async () => {
    const message = `Sign this message to authenticate.\nTimestamp: ${Date.now()}`;
    const encodedMessage = new TextEncoder().encode(message);

    const signature = await signMessage(encodedMessage);

    console.log('Signature:', signature);
    return { message, signature, publicKey: publicKey?.toBase58() };
  };

  return <button onClick={handleSign}>Sign Message</button>;
}

Authentication Flow

function AuthComponent() {
  const { signMessage, publicKey } = useWallet();
  const [isAuthenticated, setIsAuthenticated] = useState(false);

  const authenticate = async () => {
    try {
      const nonce = crypto.randomUUID();
      const message = `Sign in to MyDApp\nNonce: ${nonce}`;
      const encodedMessage = new TextEncoder().encode(message);

      const signature = await signMessage(encodedMessage);

      // Send to backend for verification
      const response = await fetch('/api/auth/verify', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          publicKey: publicKey?.toBase58(),
          message,
          signature: Array.from(signature),
        }),
      });

      if (response.ok) {
        const { token } = await response.json();
        localStorage.setItem('auth-token', token);
        setIsAuthenticated(true);
      }
    } catch (error) {
      console.error('Authentication failed:', error);
    }
  };

  return (
    <div>
      {isAuthenticated ? (
        <p>Authenticated!</p>
      ) : (
        <button onClick={authenticate}>Sign In</button>
      )}
    </div>
  );
}

Backend Verification (Conceptual)

On your backend, verify message signatures by following these steps:
  1. Receive data from client: message, signature, and public key
  2. Verify Ed25519 signature: Use a cryptographic library (e.g., tweetnacl, @noble/ed25519)
  3. Validate message contents: Check nonce, timestamp, and domain match expectations
  4. Prevent replay attacks: Store used nonces temporarily and reject duplicates
  5. Check expiration: Verify message timestamp is recent (e.g., within 5 minutes)
  6. Generate auth token: Create JWT or session token if signature is valid
Security Notes:
  • Never trust client-side verification - always verify server-side
  • Use HTTPS to protect signatures in transit
  • Rate limit verification endpoints to prevent brute force attacks
  • Store nonces with TTL to prevent replay attacks
Pseudo-code for verification:
// 1. Parse and validate input
message_bytes = utf8_encode(message)
signature_bytes = decode_signature(signature)
public_key_bytes = decode_public_key(public_key)

// 2. Verify signature
if !verify_ed25519(message_bytes, signature_bytes, public_key_bytes):
    return error("Invalid signature")

// 3. Check nonce not used
if nonce_exists_in_cache(nonce):
    return error("Nonce already used")
cache_nonce(nonce, ttl=300)  // 5 minutes

// 4. Check message not expired
if timestamp < now() - 300:  // 5 minutes
    return error("Message expired")

// 5. Generate token
token = generate_jwt(public_key)
return success(token)

Sign-In With Solana (SIWS)

function SignInWithSolana() {
  const { signIn } = useWallet();

  const handleSignIn = async () => {
    const { account, signedMessage, signature } = await signIn({
      domain: window.location.host,
      statement: 'Sign in to MyDApp',
      uri: window.location.origin,
    });

    // Verify on backend
    const response = await fetch('/api/auth/siws', {
      method: 'POST',
      body: JSON.stringify({ account, signedMessage, signature }),
    });

    const { token } = await response.json();
    localStorage.setItem('token', token);
  };

  return <button onClick={handleSignIn}>Sign In</button>;
}

Message Signing (@solana/kit)

import { useWallet, useConnection } from '@hermis/solana-headless-react';
import { createKitSignersFromAdapter } from '@hermis/solana-headless-adapter-base';
import { HermisError, HERMIS_ERROR__WALLET_INTERACTION__FEATURE_NOT_SUPPORTED } from '@hermis/errors';
import { signBytes } from '@solana/kit';

function SignMessageKit() {
  const { wallet, publicKey } = useWallet();
  const { connection } = useConnection();

  const handleSign = async () => {
    if (!wallet || !publicKey) return;

    // Create Kit signers from adapter
    const { messageSigner } = createKitSignersFromAdapter(
      wallet.adapter,
      connection
    );

    if (!messageSigner) {
      throw new HermisError(
        HERMIS_ERROR__WALLET_INTERACTION__FEATURE_NOT_SUPPORTED,
        { feature: 'message signing' }
      );
    }

    const message = `Sign this message to authenticate.\nTimestamp: ${Date.now()}`;
    const messageBytes = new TextEncoder().encode(message);

    // Sign using Kit message signer
    const signatures = await signBytes(messageSigner, messageBytes);
    const signature = signatures[messageSigner.address];

    console.log('Signature:', signature);
    return {
      message,
      signature: Array.from(signature),
      publicKey: publicKey.toBase58(),
    };
  };

  return <button onClick={handleSign}>Sign Message (Kit)</button>;
}
When to use Kit for message signing:
  • Building with @solana/kit architecture
  • Need type-safe signer interfaces
  • Working with Kit-native libraries
  • Want better tree-shaking and smaller bundles
Note: The Kit messageSigner integrates seamlessly with wallet adapters through createKitSignersFromAdapter, providing a bridge between traditional wallet adapters and modern Kit signers.