Skip to main content

Try it Live

Run Secp256k1 examples in the interactive playground
This page is a placeholder. All examples on this page are currently AI-generated and are not correct. This documentation will be completed in the future with accurate, tested examples.

Secp256k1 Security

Security considerations, attack vectors, and best practices for elliptic curve cryptography with secp256k1.

Critical Warnings

⚠️ Zig Implementation NOT Audited The Zig implementation (src/crypto/secp256k1.zig) is:
  • UNAUDITED - No security review
  • NOT constant-time - Vulnerable to timing attacks
  • Educational only - Do not use in production
For production, use:
  1. TypeScript implementation (@noble/curves) - audited
  2. Hardware wallets (Ledger, Trezor)
  3. EVM precompiles (ecRecover)

Private Key Security

Generation

Use cryptographically secure random: Correct:
// Browser
const privateKey = Bytes32();
crypto.getRandomValues(privateKey);

// Node.js
import crypto from 'crypto';
const privateKey = crypto.randomBytes(32);

// Hardware wallet
// Keys generated in secure hardware, never exported
Never:
// Math.random() is NOT cryptographic
for (let i = 0; i < 32; i++) {
  privateKey[i] = Math.floor(Math.random() * 256); // ❌ INSECURE
}

// Timestamps have low entropy
const timestamp = Date.now();
const hash = keccak256(numberToBytes(timestamp)); // ❌ PREDICTABLE

// User input alone has low entropy
const password = "mypassword123";
const key = keccak256(stringToBytes(password)); // ❌ WEAK
Entropy requirements:
  • Minimum 256 bits (32 bytes) of cryptographic randomness
  • Use OS-provided CSPRNG (crypto.getRandomValues)
  • Hardware RNG preferred (TPM, Secure Enclave)
  • For offline: dice rolls + hashing (256 rolls * 2.58 bits = 660 bits)

Storage

⚠️ Protect keys at rest: Best practices:
  • Hardware wallets: Store in Ledger/Trezor, never export
  • Encrypted keystores: AES-256-GCM with strong KDF (scrypt/argon2)
  • Secure Enclave: iOS/macOS hardware-backed storage
  • HSM: Enterprise hardware security modules
  • Environment isolation: Air-gapped for high-value keys
Avoid:
  • Plain text files
  • Environment variables (leak in logs)
  • Git repositories (permanent history)
  • Clipboard (malware can read)
  • Screenshots (OCR readable)
  • Cloud storage unencrypted
  • Browser localStorage unencrypted

Key Derivation

Use BIP32/BIP39 for backups:
import * as Bip39 from '@tevm/voltaire/crypto/Bip39';
import * as HDWallet from '@tevm/voltaire/crypto/HDWallet';

// Generate mnemonic (12-24 words)
const mnemonic = Bip39.generateMnemonic(256); // 24 words

// Derive master key
const seed = Bip39.mnemonicToSeed(mnemonic);
const masterKey = HDWallet.fromSeed(seed);

// Derive account keys (BIP44)
const accountKey = HDWallet.derivePath(masterKey, "m/44'/60'/0'/0/0");

// Backup: Write down 24 words, never private keys directly

Nonce Security

RFC 6979 Deterministic Nonces

Why deterministic? Random nonce generation has catastrophic failure modes: Nonce reuse leaks private key:
Sign message1 with nonce k: s1 = k^-1 * (e1 + r * privkey)
Sign message2 with nonce k: s2 = k^-1 * (e2 + r * privkey)

Solve for privkey:
k = (e1 - e2) / (s1 - s2)
privkey = (s1*k - e1) / r
Real attacks:
  • PlayStation 3 hack (2010) - Sony reused k=4
  • Bitcoin theft - Bad RNG in Android wallet (2013)
  • Blockchain.info bug (2014) - Weak Java SecureRandom
RFC 6979 prevents this:
k = HMAC_DRBG(key: privkey, data: message_hash)
Benefits:
  • Same (message, key) always produces same nonce
  • No RNG required
  • Deterministic = testable
  • HMAC_DRBG provides cryptographic strength

Implementation Requirements

All implementations MUST:
  • Use RFC 6979 deterministic nonces
  • NEVER allow custom nonce input
  • NEVER reuse nonces across different messages
  • Validate nonce is in range [1, n-1]

Signature Malleability

Problem

ECDSA signatures have inherent malleability:
// Both signatures verify for same (message, publicKey)
const sig1 = { r, s: s, v: 27 };
const sig2 = { r, s: n - s, v: 28 }; // Malleated
Attacks:
  • Transaction replay with modified txHash
  • Smart contract vulnerabilities (signature-based authentication)
  • Blockchain state inconsistency

Solution: Low-s Enforcement

Ethereum enforces s ≤ n/2 (BIP 62, EIP-2):
const HALF_N = SECP256K1_N >> 1n;

if (s > HALF_N) {
  s = SECP256K1_N - s;  // Normalize to low-s
  v ^= 1;  // Flip recovery ID
}
Our implementation:
  • sign() always produces low-s
  • verify() rejects high-s signatures
  • recoverPublicKey() rejects high-s

Side-Channel Attacks

Timing Attacks

Non-constant-time implementations leak secrets via execution time: Vulnerable:
// Branch timing leaks bit values
if (bit == 1) {
  result = result + addend;  // Takes longer
}
Constant-time:
// Same timing regardless of bit value
mask = -(bit & 1);  // 0x00000000 or 0xFFFFFFFF
result = result + (addend & mask);  // Always executes
Attack scenario:
Measure signing time for different messages
→ Deduce private key bits from timing variations
→ After ~1000 signatures, recover full key
Mitigations:
  • Use constant-time libraries (@noble/curves ✅)
  • Avoid conditional branches on secrets
  • Use hardware wallets (constant-time guaranteed)
  • Avoid Zig implementation (⚠️ NOT constant-time)

Power Analysis

Differential Power Analysis (DPA):
  • Measure CPU power consumption during crypto ops
  • Correlate power spikes with bit operations
  • Recover secret keys after many measurements
Simple Power Analysis (SPA):
  • Single power trace reveals operation sequence
  • Identify point additions vs doublings
  • Reconstruct scalar multiplication pattern
Mitigations:
  • Hardware security (HSM, Secure Enclave)
  • Power randomization
  • Constant-time algorithms
  • Blinding techniques

Cache Timing Attacks

Memory access patterns leak information via cache hits/misses: Vulnerable:
// Table lookup leaks index via cache timing
value = precomputed_table[secret_index];
Secure:
// Load entire table (constant-time access)
for (int i = 0; i < table_size; i++) {
  mask = -(i == secret_index);
  value |= precomputed_table[i] & mask;
}
Real attacks:
  • Flush+Reload on AES T-tables
  • Prime+Probe on RSA/ECC

Message Hashing

Always Hash Before Signing

⚠️ Sign hashes, not raw messages: Vulnerable:
// Signing raw message allows chosen-plaintext attacks
const signature = Secp256k1.sign(rawMessage, privateKey);
Secure:
// Hash message first (collision-resistant)
const messageHash = Keccak256.hash(rawMessage);
const signature = Secp256k1.sign(messageHash, privateKey);
Why?
  • ECDSA security requires random-looking messages
  • Raw messages may have structure attackers exploit
  • Hashing provides collision resistance
  • Fixed-length input simplifies validation

Hash Function Requirements

Use collision-resistant hash functions: Approved:
  • Keccak256 (Ethereum standard)
  • SHA-256 (Bitcoin, general use)
  • SHA-3 (NIST standard)
  • Blake2b (high performance)
Deprecated:
  • MD5 (broken - collisions trivial)
  • SHA-1 (broken - collisions practical)

Public Key Validation

Always Validate Public Keys

⚠️ Verify points are on curve:
function isValidPublicKey(pubkey: Uint8Array): boolean {
  if (pubkey.length !== 64) return false;

  const x = bytesToBigInt(pubkey.slice(0, 32));
  const y = bytesToBigInt(pubkey.slice(32, 64));

  // Check y² = x³ + 7 (mod p)
  const p = SECP256K1_P;
  const y2 = (y * y) % p;
  const x3_plus_7 = (x * x * x + 7n) % p;

  return y2 === x3_plus_7;
}
Attacks if unvalidated:
  • Invalid curve attacks (point not on secp256k1)
  • Small subgroup attacks
  • Twist attacks (point on quadratic twist)

Check for Point at Infinity

// Reject point at infinity (identity element)
if (x === 0n && y === 0n) return false;

Ethereum-Specific Considerations

EIP-155 Replay Protection

Problem: Signatures valid on one chain can be replayed on forks. Solution: Include chainId in v value:
// Pre-EIP-155 (vulnerable to replay)
v = recoveryId + 27;

// Post-EIP-155 (replay protected)
v = chainId * 2 + 35 + recoveryId;

// Examples:
// Ethereum mainnet (chainId=1): v = 37 or 38
// Goerli testnet (chainId=5): v = 45 or 46

ecRecover Gotchas

Precompile behavior:
  • Returns zero address on invalid signature (NOT error)
  • Always check return value != 0x0
function verifySigner(bytes32 hash, uint8 v, bytes32 r, bytes32 s, address expected)
  public pure returns (bool)
{
  address signer = ecrecover(hash, v, r, s);

  // ⚠️ Check for zero address (invalid signature)
  if (signer == address(0)) return false;

  return signer == expected;
}

EIP-191 Personal Sign

Prefix prevents signing raw transactions:
// Without prefix: attacker could trick user into signing transaction
const hash = keccak256(transaction);  // ❌ Dangerous

// With prefix: clearly marked as non-transaction message
const prefix = `\x19Ethereum Signed Message:\n${message.length}`;
const hash = keccak256(prefix + message);  // ✅ Safe

Best Practices Summary

DO

✅ Use hardware wallets for high-value keys ✅ Use @noble/curves (audited) for TypeScript ✅ Generate keys with crypto.getRandomValues() ✅ Store encrypted keystores (AES-256-GCM + scrypt) ✅ Validate all inputs (keys, signatures, hashes) ✅ Use RFC 6979 deterministic nonces ✅ Enforce low-s malleability protection ✅ Hash messages before signing ✅ Include EIP-155 chainId in signatures ✅ Use BIP39/BIP32 for backups ✅ Test with official test vectors

DON’T

❌ Use Math.random() for key generation ❌ Reuse nonces across messages ❌ Store private keys unencrypted ❌ Sign raw messages without hashing ❌ Skip public key validation ❌ Use Zig implementation in production (unaudited) ❌ Implement custom crypto (use audited libraries) ❌ Trust user-provided public keys without validation ❌ Ignore signature malleability ❌ Forget EIP-155 replay protection

Security Checklist

  • Keys generated with CSPRNG
  • Keys stored encrypted or in hardware
  • Using audited library (@noble/curves)
  • RFC 6979 deterministic nonces
  • Low-s enforcement enabled
  • Public keys validated (point on curve)
  • Messages hashed before signing
  • EIP-155 chainId in signatures
  • Test vectors passing
  • No custom crypto implementation
  • Side-channel mitigations in place
  • ecRecover zero-address checks