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
| Property | Description |
|---|---|
| Authentication | Proves who created the signature |
| Integrity | Any change to message invalidates signature |
| Non-repudiation | Signer cannot deny signing |
| Uniqueness | Same 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
| Practice | Description |
|---|---|
| Include nonce | Prevent signature replay attacks |
| Add timestamp | Signatures should expire |
| Specify domain | Prevent cross-site signature reuse |
| Verify before trust | Always verify signatures server-side |
| Use standard formats | SIWS, EIP-4361 for interoperability |
Next: Ed25519 Algorithm - Deep dive into Solana's signature algorithm.