Digital Signatures

Digital signatures provide authentication, integrity, and non-repudiation—proving who signed a message and that it hasn't been tampered with.

How Signatures Work

Text
┌─────────────────────────────────────────────────────────────────┐
│                   Digital Signature Flow                        │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  SIGNING (Private Key)                                          │
│  ┌─────────────┐     ┌──────────────┐     ┌─────────────┐      │
│  │   Message   │ ──▶ │  Hash (SHA)  │ ──▶ │  Encrypt    │      │
│  │  "Hello"    │     │  256 bits    │     │  with       │      │
│  └─────────────┘     └──────────────┘     │  Private    │      │
│                                           │  Key        │      │
│                                           └──────┬──────┘      │
│                                                  │              │
│                                                  ▼              │
│                                           ┌─────────────┐      │
│                                           │  Signature  │      │
│                                           │  64 bytes   │      │
│                                           └─────────────┘      │
│                                                                 │
│  VERIFYING (Public Key)                                         │
│  ┌─────────────┐     ┌──────────────┐                          │
│  │   Message   │ ──▶ │  Hash (SHA)  │ ──▶ Compare              │
│  │  "Hello"    │     │  256 bits    │        │                 │
│  └─────────────┘     └──────────────┘        │                 │
│                                              │                 │
│  ┌─────────────┐     ┌──────────────┐        │                 │
│  │  Signature  │ ──▶ │  Decrypt     │ ──▶────┘                 │
│  │  64 bytes   │     │  with Public │                          │
│  └─────────────┘     │  Key         │     ✓ Valid /Invalid  │
│                      └──────────────┘                          │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Signature Properties

PropertyDescription
AuthenticationProves who created the signature
IntegrityAny change to message invalidates signature
Non-repudiationSigner cannot deny signing
UniquenessSame message + key = same signature (Ed25519)

Ed25519 Signatures

TypeScript
import { Keypair } from "@solana/web3.js";
import nacl from "tweetnacl";

// Generate keypair
const keypair = Keypair.generate();

// Message to sign
const message = Buffer.from("Transfer 100 SOL to Bob");

// Sign the message
const signature = nacl.sign.detached(message, keypair.secretKey);
console.log("Signature:", Buffer.from(signature).toString("hex"));
// 64 bytes

// Verify the signature
const isValid = nacl.sign.detached.verify(
  message,
  signature,
  keypair.publicKey.toBytes(),
);
console.log("Valid:", isValid); // true

// Tampered message fails verification
const tamperedMessage = Buffer.from("Transfer 1000 SOL to Bob");
const isValidTampered = nacl.sign.detached.verify(
  tamperedMessage,
  signature,
  keypair.publicKey.toBytes(),
);
console.log("Tampered valid:", isValidTampered); // false

Transaction Signing on Solana

TypeScript
import {
  Connection,
  Keypair,
  PublicKey,
  SystemProgram,
  Transaction,
  sendAndConfirmTransaction,
} from "@solana/web3.js";

async function signAndSendTransaction(
  connection: Connection,
  sender: Keypair,
  recipient: PublicKey,
  lamports: number,
): Promise<string> {
  // Create transaction
  const transaction = new Transaction().add(
    SystemProgram.transfer({
      fromPubkey: sender.publicKey,
      toPubkey: recipient,
      lamports,
    }),
  );

  // Get recent blockhash (required for transaction)
  const { blockhash } = await connection.getLatestBlockhash();
  transaction.recentBlockhash = blockhash;
  transaction.feePayer = sender.publicKey;

  // Sign the transaction
  transaction.sign(sender);

  // Verify signature before sending
  const isVerified = transaction.verifySignatures();
  console.log("Signatures valid:", isVerified);

  // Send and confirm
  const signature = await sendAndConfirmTransaction(connection, transaction, [
    sender,
  ]);

  return signature;
}

// Multiple signers
async function multiSignTransaction(
  connection: Connection,
  signers: Keypair[],
  instructions: TransactionInstruction[],
): Promise<string> {
  const transaction = new Transaction().add(...instructions);

  const { blockhash } = await connection.getLatestBlockhash();
  transaction.recentBlockhash = blockhash;
  transaction.feePayer = signers[0].publicKey;

  // All signers must sign
  for (const signer of signers) {
    transaction.partialSign(signer);
  }

  // Verify all required signatures are present
  if (!transaction.verifySignatures()) {
    throw new Error("Missing required signatures");
  }

  const signature = await connection.sendRawTransaction(
    transaction.serialize(),
  );

  return signature;
}

Offline Signing

TypeScript
import {
  Connection,
  Keypair,
  PublicKey,
  SystemProgram,
  Transaction,
} from "@solana/web3.js";

// Step 1: Create unsigned transaction (online machine)
async function createUnsignedTransaction(
  connection: Connection,
  sender: PublicKey,
  recipient: PublicKey,
  lamports: number,
): Promise<string> {
  const transaction = new Transaction().add(
    SystemProgram.transfer({
      fromPubkey: sender,
      toPubkey: recipient,
      lamports,
    }),
  );

  const { blockhash, lastValidBlockHeight } =
    await connection.getLatestBlockhash();
  transaction.recentBlockhash = blockhash;
  transaction.feePayer = sender;

  // Serialize without signatures
  const serialized = transaction
    .serialize({
      requireAllSignatures: false,
      verifySignatures: false,
    })
    .toString("base64");

  return serialized;
}

// Step 2: Sign offline (air-gapped machine)
function signOffline(serializedTransaction: string, signer: Keypair): string {
  const transaction = Transaction.from(
    Buffer.from(serializedTransaction, "base64"),
  );

  transaction.sign(signer);

  return transaction.serialize().toString("base64");
}

// Step 3: Submit signed transaction (online machine)
async function submitSignedTransaction(
  connection: Connection,
  signedTransaction: string,
): Promise<string> {
  const buffer = Buffer.from(signedTransaction, "base64");
  const signature = await connection.sendRawTransaction(buffer);
  return signature;
}

Signature Aggregation

TypeScript
// Verifying multiple signatures efficiently
interface SignedMessage {
  message: Buffer;
  signature: Uint8Array;
  publicKey: Uint8Array;
}

function batchVerify(signedMessages: SignedMessage[]): boolean[] {
  return signedMessages.map((sm) =>
    nacl.sign.detached.verify(sm.message, sm.signature, sm.publicKey),
  );
}

// All must be valid
function verifyAll(signedMessages: SignedMessage[]): boolean {
  return signedMessages.every((sm) =>
    nacl.sign.detached.verify(sm.message, sm.signature, sm.publicKey),
  );
}

// Threshold: at least N valid
function verifyThreshold(
  signedMessages: SignedMessage[],
  threshold: number,
): boolean {
  const validCount = signedMessages.filter((sm) =>
    nacl.sign.detached.verify(sm.message, sm.signature, sm.publicKey),
  ).length;

  return validCount >= threshold;
}

Message Signing for Authentication

TypeScript
import { Keypair } from "@solana/web3.js";
import nacl from "tweetnacl";
import bs58 from "bs58";

// Sign-In with Solana (SIWS) pattern
interface SignInMessage {
  domain: string;
  address: string;
  statement: string;
  nonce: string;
  issuedAt: string;
  expirationTime?: string;
}

function createSignInMessage(params: SignInMessage): string {
  let message = `${params.domain} wants you to sign in with your Solana account:\n`;
  message += `${params.address}\n\n`;

  if (params.statement) {
    message += `${params.statement}\n\n`;
  }

  message += `Nonce: ${params.nonce}\n`;
  message += `Issued At: ${params.issuedAt}`;

  if (params.expirationTime) {
    message += `\nExpiration Time: ${params.expirationTime}`;
  }

  return message;
}

// Client-side: Sign the message
function signMessage(message: string, keypair: Keypair): string {
  const messageBytes = Buffer.from(message);
  const signature = nacl.sign.detached(messageBytes, keypair.secretKey);
  return bs58.encode(signature);
}

// Server-side: Verify the signature
function verifySignedMessage(
  message: string,
  signatureBase58: string,
  publicKeyBase58: string,
): boolean {
  const messageBytes = Buffer.from(message);
  const signature = bs58.decode(signatureBase58);
  const publicKey = bs58.decode(publicKeyBase58);

  return nacl.sign.detached.verify(messageBytes, signature, publicKey);
}

// Full authentication flow
async function authenticateUser(
  signedMessage: string,
  signature: string,
  publicKey: string,
): Promise<{ authenticated: boolean; address: string }> {
  // Verify signature
  const isValid = verifySignedMessage(signedMessage, signature, publicKey);

  if (!isValid) {
    return { authenticated: false, address: "" };
  }

  // Parse and validate message contents
  const lines = signedMessage.split("\n");
  const address = lines[1];

  // Verify address matches public key
  if (address !== publicKey) {
    return { authenticated: false, address: "" };
  }

  // Check expiration
  const expirationLine = lines.find((l) => l.startsWith("Expiration Time:"));
  if (expirationLine) {
    const expiration = new Date(expirationLine.split(": ")[1]);
    if (expiration < new Date()) {
      return { authenticated: false, address: "" };
    }
  }

  return { authenticated: true, address };
}

Signature Malleability

TypeScript
// Ed25519 signatures are NOT malleable by design
// But be careful with other schemes

// ECDSA (secp256k1) signatures ARE malleable:
// Given signature (r, s), (r, -s mod n) is also valid
// This is why Bitcoin uses "low S" values

// Solana uses Ed25519 which prevents malleability:
// - Deterministic signatures (same message + key = same signature)
// - Signature uniqueness is guaranteed
// - No need for normalization

// Always use canonical signatures
function isCanonicalSignature(signature: Uint8Array): boolean {
  // Ed25519 signatures are always canonical
  // For ECDSA, check if S is in lower half
  return signature.length === 64;
}

Common Pitfalls

TypeScript
// ❌ BAD: Signing without proper message formatting
const badSignature = nacl.sign.detached(
  Buffer.from("send money"), // Vague, replayable
  keypair.secretKey,
);

// ✅ GOOD: Include context and uniqueness
const goodMessage = JSON.stringify({
  action: "transfer",
  amount: 100,
  recipient: "Bob...",
  nonce: crypto.randomUUID(), // Prevent replay
  timestamp: Date.now(),
  chainId: "mainnet-beta", // Network-specific
});
const goodSignature = nacl.sign.detached(
  Buffer.from(goodMessage),
  keypair.secretKey,
);

// ❌ BAD: Not verifying before trusting
function processPayment(signature: string) {
  // Process without verification!
}

// ✅ GOOD: Always verify
function processPayment(message: string, signature: string, publicKey: string) {
  if (!verifySignedMessage(message, signature, publicKey)) {
    throw new Error("Invalid signature");
  }
  // Now safe to process
}

Best Practices

PracticeDescription
Include noncePrevent signature replay attacks
Add timestampSignatures should expire
Specify domainPrevent cross-site signature reuse
Verify before trustAlways verify signatures server-side
Use standard formatsSIWS, EIP-4361 for interoperability

Next: Ed25519 Algorithm - Deep dive into Solana's signature algorithm.