Skip to main content

Try it Live

Run SIWE examples in the interactive playground

Verification

Signature verification for SIWE messages.

verify

Verify SIWE message signature matches claimed address.

Signature

function verify(
  message: BrandedMessage,
  signature: Signature
): boolean

Parameters

  • message - BrandedMessage that was signed
  • signature - 65-byte signature (r + s + v)

Returns

true if signature valid and matches message.address false if signature invalid or address mismatch

Signature Format

Bytes 0-31:  r (32 bytes) - ECDSA signature component
Bytes 32-63: s (32 bytes) - ECDSA signature component
Byte 64:     v (1 byte)   - Recovery ID (0, 1, 27, or 28)
Total: 65 bytesV normalization:
  • If v >= 27: recoveryId = v - 27
  • If v < 27: recoveryId = v
  • Valid recovery IDs: 0 or 1

Process

  1. Validate Message: Check structure with validate(message)
  2. Get Hash: Compute EIP-191 hash with getMessageHash(message)
  3. Extract Components: Parse r, s, v from signature
  4. Normalize V: Convert v to recovery ID (0 or 1)
  5. Recover Public Key: Use secp256k1 recovery
  6. Derive Address: Keccak-256(publicKey)[12:32]
  7. Compare: Check recovered address === message.address

Example

const message = Siwe.create({
  domain: "example.com",
  address: userAddress,
  uri: "https://example.com",
  chainId: 1,
});

const text = Siwe.format(message);
const signature = await wallet.signMessage(text);

const valid = Siwe.verify(message, signature);
if (valid) {
  console.log("Signature verified, user authenticated");
} else {
  console.log("Invalid signature");
}

Common Patterns

Backend Verification

app.post('/auth/verify', async (req, res) => {
  try {
    const message = Siwe.parse(req.body.message);
    const signature = hexToBytes(req.body.signature);

    const valid = Siwe.verify(message, signature);
    if (valid) {
      createSession(message.address);
      res.json({ success: true });
    } else {
      res.status(401).json({ error: "Invalid signature" });
    }
  } catch (err) {
    res.status(400).json({ error: "Invalid request" });
  }
});

Instance Method

const message = Siwe.create({ ... });
const valid = message.verify(signature);
// Same as Siwe.verify(message, signature)

With Validation

// Validate before expensive verification
const validationResult = Siwe.validate(message);
if (!validationResult.valid) {
  return error("Invalid message structure");
}

const signatureValid = Siwe.verify(message, signature);
if (!signatureValid) {
  return error("Signature mismatch");
}

Error Cases

Returns false for:
  • Invalid message structure
  • Signature length !== 65 bytes
  • Invalid v value (not 0, 1, 27, or 28)
  • Failed public key recovery
  • Address mismatch
  • Any exception during verification

Security Considerations

Constant-Time Comparison:
  • Address comparison loops through all 20 bytes
  • Prevents timing attacks
V Normalization:
  • Accepts both raw (0, 1) and EIP-155 (27, 28) formats
  • Rejects invalid v values
Public Key Recovery:
  • Uses secp256k1 curve (same as Ethereum)
  • Validates recovered point on curve
  • Rejects invalid signatures
No Malleability:
  • Signature uniqueness enforced by secp256k1
  • Low-s values recommended but not required

verifyMessage

Combined validation and signature verification.

Signature

function verifyMessage(
  message: BrandedMessage,
  signature: Signature,
  options?: { now?: Date }
): ValidationResult

Parameters

  • message - BrandedMessage to verify
  • signature - 65-byte signature
  • options.now - Current time for timestamp checks

Returns

ValidationResult:
  • { valid: true } - Structure valid AND signature verified
  • { valid: false, error: ValidationError } - Structure invalid OR signature mismatch

Process

  1. Validate Structure: Call validate(message, options)
  2. Return if Invalid: Early return with validation error
  3. Verify Signature: Call verify(message, signature)
  4. Return Result: { valid: true } or signature mismatch error

Example

const message = Siwe.parse(req.body.message);
const signature = hexToBytes(req.body.signature);

const result = Siwe.verifyMessage(message, signature);

if (result.valid) {
  // Message structure valid AND signature verified
  createSession(message.address);
} else {
  // Either structure invalid or signature mismatch
  console.error(result.error.type);
  console.error(result.error.message);
}

Error Types

All validation errors plus:
  • signature_mismatch - Signature does not match address or verification failed

Common Patterns

Complete Verification

const result = Siwe.verifyMessage(message, signature);

if (!result.valid) {
  switch (result.error.type) {
    case "signature_mismatch":
      logSecurityEvent("Invalid signature", { address: message.address });
      return unauthorized();
    case "expired":
      return reauth("Session expired");
    case "not_yet_valid":
      return error("Authentication not yet valid");
    case "invalid_nonce":
      return error("Invalid nonce");
    default:
      return error(result.error.message);
  }
}

// Authenticated
createSession(message.address);

With Timestamp Check

const result = Siwe.verifyMessage(message, signature, {
  now: new Date(),
});

if (result.valid) {
  // Message not expired, not before satisfied, signature valid
  authenticateUser(message.address);
}

Complete Auth Flow

async function authenticateWithSiwe(
  messageText: string,
  signatureHex: string
): Promise<{ success: boolean; error?: string }> {
  try {
    // Parse message
    const message = Siwe.parse(messageText);

    // Verify nonce
    if (!await verifyNonce(message.nonce)) {
      return { success: false, error: "Invalid or reused nonce" };
    }

    // Verify domain
    if (message.domain !== expectedDomain) {
      return { success: false, error: "Domain mismatch" };
    }

    // Convert signature
    const signature = hexToBytes(signatureHex);

    // Verify message and signature
    const result = Siwe.verifyMessage(message, signature);

    if (!result.valid) {
      return { success: false, error: result.error.message };
    }

    // Create session
    await createSession(message.address, message.chainId);
    return { success: true };

  } catch (err) {
    return { success: false, error: "Authentication failed" };
  }
}

Advantages Over Separate Calls

Convenience:
  • Single function call for complete verification
  • Structured error handling
  • Consistent return type
Efficiency:
  • Early return on validation failure
  • Skips expensive signature verification if structure invalid
Correctness:
  • Ensures validation always happens first
  • Prevents verification of malformed messages

Performance

Validation: O(1) - Fast structure checks Signature Verification: O(1) - secp256k1 recovery (expensive) Optimization: Always validates first to avoid wasting cycles on invalid messages

Factory API

Tree-shakeable factory pattern with explicit crypto dependencies.

Verify Factory

function Verify({
  keccak256,
  secp256k1RecoverPublicKey,
  addressFromPublicKey
}): (message: BrandedMessage, signature: Signature) => boolean
Dependencies:
  • keccak256: (data: Uint8Array) => Uint8Array - Keccak256 hash function
  • secp256k1RecoverPublicKey: (sig: {r, s, v}, hash: Uint8Array) => Uint8Array - secp256k1 public key recovery
  • addressFromPublicKey: (x: bigint, y: bigint) => Uint8Array - Address derivation from public key
Example:
import { Verify } from 'tevm/Siwe'
import { hash as keccak256 } from 'tevm/crypto/Keccak256'
import { recoverPublicKey as secp256k1RecoverPublicKey } from 'tevm/crypto/Secp256k1'
import { FromPublicKey } from 'tevm/Address'

const addressFromPublicKey = FromPublicKey({ keccak256 })
const verify = Verify({ keccak256, secp256k1RecoverPublicKey, addressFromPublicKey })

const valid = verify(message, signature)
Bundle size: Crypto only included if you import it.

VerifyMessage Factory

function VerifyMessage({
  keccak256,
  secp256k1RecoverPublicKey,
  addressFromPublicKey
}): (message: BrandedMessage, signature: Signature, options?: {now?: Date}) => ValidationResult
Same dependencies as Verify. Example:
import { VerifyMessage } from 'tevm/Siwe'
import { hash as keccak256 } from 'tevm/crypto/Keccak256'
import { recoverPublicKey as secp256k1RecoverPublicKey } from 'tevm/crypto/Secp256k1'
import { FromPublicKey } from 'tevm/Address'

const addressFromPublicKey = FromPublicKey({ keccak256 })
const verifyMessage = VerifyMessage({ keccak256, secp256k1RecoverPublicKey, addressFromPublicKey })

const result = verifyMessage(message, signature, { now: new Date() })

GetMessageHash Factory

function GetMessageHash({
  keccak256
}): (message: BrandedMessage) => Uint8Array
Dependencies:
  • keccak256: (data: Uint8Array) => Uint8Array - Keccak256 hash function
Example:
import { GetMessageHash } from 'tevm/Siwe'
import { hash as keccak256 } from 'tevm/crypto/Keccak256'

const getMessageHash = GetMessageHash({ keccak256 })
const hash = getMessageHash(message)

See Also