A secure, policy-enforced remote signer for Ethereum transactions using ESP32 hardware. This project provides a low-cost alternative to expensive Hardware Security Modules (HSMs) for market-making bots and automated trading systems.
This is NOT a tamper-proof HSM. This device is suitable for hot balances with limited blast radius under strict policy enforcement. See Security Model for details.
- 🔐 Hardware-isolated private keys - Keys never leave the ESP32 device
- 📋 Policy enforcement - Address whitelisting, gas caps, function selector filtering
- 🔐 Secure Boot & Flash Encryption - Production-grade ESP32 security features
- 🌐 HTTPS API - Challenge-response HMAC authentication
- ⚡ EIP-155/EIP-1559 support - Modern Ethereum transaction formats
- 🛡️ Rate limiting - Configurable request limits and abuse prevention
- 📦 Node.js client - Drop-in ethers.js Signer replacement
- 🔄 Dual mode operation - Provisioning mode for setup, signing mode for operation
- 🏠 mDNS hostname support - Configurable network discovery via hostname.local
Run the firmware in QEMU emulator for development and testing:
# Install QEMU ESP32 (macOS)
brew install libgcrypt glib pixman sdl2 libslirp
idf_tools.py install qemu-xtensa qemu-riscv32
# Build and run in emulator
make emulator-setup
# Or run in provisioning mode
make emulator-run-provisioningSee Emulator Setup Guide for details.
-
Required Hardware:
- ESP32 NodeMCU-32S (or compatible with Secure Boot support)
- 1 jumper wire for provisioning mode
- USB cable for programming
-
GPIO Connections:
Provisioning Jumper: GPIO 2 ──── GND (for provisioning mode)
-
Install ESP-IDF:
# Install ESP-IDF v5.x git clone --recursive https://github.com/espressif/esp-idf.git cd esp-idf ./install.sh . ./export.sh
-
Generate SSL Certificates:
git clone https://github.com/your-org/esp32-remote-signer.git cd esp32-remote-signer # Generate self-signed certificates for HTTPS server cd main/certs openssl req -newkey rsa:2048 -nodes -keyout server_key.pem -x509 -days 3650 \ -out server_cert.pem -subj "/CN=ESP32RemoteSigner" cd ../..
-
Build and Flash:
# Development build make dev-build make flash # Production build (with secure boot) make generate-keys make prod-build make flash-secure
-
Enter Provisioning Mode:
- Connect GPIO 2 to GND
- Power on the device
- Device creates WiFi access point:
ESP32-Signer-XXXX
-
Configure via Web Interface:
- Connect your phone/laptop to the
ESP32-Signer-XXXXnetwork - Open any website in your browser (captive portal will redirect)
- Or directly visit:
https://192.168.4.1 - Complete setup form:
- WiFi credentials for your network
- Device hostname (optional, defaults to "espwarden")
- 256-bit authentication key (64 hex characters)
- Generate signing key
- Set transaction policy (chains, addresses, limits)
- Connect your phone/laptop to the
-
Complete Setup:
- Click "Restart Device" or remove GPIO 2 jumper and power cycle
- Device will connect to your WiFi network
- Access device via IP address or mDNS hostname (e.g.,
espwarden.localormysigner.local)
For programmatic setup, use the Node.js client:
const { ESP32Client } = require('@esp32/remote-signer-client');
const client = new ESP32Client({
deviceUrl: 'https://192.168.4.1', // AP mode IP
authKey: 'your-256-bit-hex-auth-key',
clientId: 'provisioning-client'
});
// Configure via API endpoints
await client.configureWiFi({ ssid: 'YourNetwork', password: 'pass' });
await client.configureHostname({ hostname: 'my-esp32-signer' });
await client.configureAuth({ key: 'your-auth-key' });
await client.configureKey({ mode: 'generate' });
await client.configurePolicy({ /* policy config */ });const { ESP32Signer } = require('@esp32/remote-signer-client');
const { JsonRpcProvider } = require('ethers');
// Create provider
const provider = new JsonRpcProvider('https://mainnet.infura.io/v3/your-key');
// Create ESP32 signer (using mDNS hostname or IP address)
const signer = new ESP32Signer({
deviceUrl: 'https://my-esp32-signer.local', // or 'https://192.168.1.100'
authKey: 'your-256-bit-hex-auth-key',
clientId: 'trading-bot-1'
}, provider);
// Use like any ethers.js signer
const tx = await signer.sendTransaction({
to: '0x742d35Cc3672C1BfeE3d4D5a0e6E9C4FfBe7E8A8',
value: ethers.parseEther('0.01'),
data: '0xa9059cbb000000000000000000000000742d35cc3672c1bfee3d4d5a0e6e9c4ffbe7e8a80000000000000000000000000000000000000000000000000de0b6b3a7640000'
});
console.log('Transaction hash:', tx.hash);GET /health- Device health and nonce for authenticationGET /info- Device information and configuration statusPOST /unlock- Authenticate and get session token
POST /wifi- Configure WiFi credentialsPOST /hostname- Set device hostname for mDNS discoveryPOST /auth- Set authentication passwordPOST /key- Generate or import private keyPOST /policy- Set transaction policyPOST /wipe- Factory reset device
POST /sign/eip1559- Sign EIP-1559 transactionPOST /sign/eip155- Sign legacy EIP-155 transaction
const { ESP32Signer, ESP32Client } = require('@esp32/remote-signer-client');
// Configuration
const config = {
deviceUrl: 'https://device-ip-address',
authKey: 'hex-auth-key',
clientId: 'unique-client-id',
timeout: 30000, // Request timeout
retryOptions: {
maxRetries: 3,
baseDelay: 1000,
maxDelay: 30000,
factor: 2
}
};
// ESP32Signer (ethers.js compatible)
const signer = new ESP32Signer(config, provider);
await signer.getAddress();
await signer.signTransaction(txRequest);
// ESP32Client (direct API access)
const client = new ESP32Client(config);
await client.getHealth();
await client.getInfo();
await client.signEIP1559(transaction);The ESP32 Remote Signer supports configurable mDNS hostnames for easy network discovery:
Provisioning Mode:
- Device accessible at fixed IP:
192.168.4.1 - mDNS disabled (not needed in AP mode)
- Hostname configurable via web interface or API
Signing Mode:
- Device joins your WiFi network
- mDNS enabled for hostname.local discovery
- Default:
espwarden.local - Custom:
your-custom-name.local
Configuration:
Via Web Interface:
1. Connect to device WiFi: ESP32-Signer-XXXX
2. Open https://192.168.4.1
3. Set hostname in "Device Hostname" section
4. Complete other provisioning steps
5. Device accessible as hostname.local after reboot
Via API:
await client.configureHostname({ hostname: 'trading-bot-1' });
// Device will be accessible as trading-bot-1.localVia Client Library:
// Use hostname for persistent device access
const signer = new ESP32Signer({
deviceUrl: 'https://trading-bot-1.local',
authKey: 'your-auth-key',
clientId: 'client-1'
}, provider);Benefits:
- No need to track changing IP addresses
- Persistent device identity across DHCP renewals
- Human-readable device identification
- Works with any mDNS-capable network
- ✅ Private key extraction from compromised host systems
- ✅ Unauthorized transactions outside policy parameters
- ✅ Replay attacks with nonce-based authentication
- ✅ Man-in-the-middle attacks (HTTPS with cert pinning)
- ✅ Excessive transaction volume (rate limiting)
- ❌ Physical access to the device
- ❌ Side-channel attacks on ESP32 hardware
- ❌ Attacks on the policy configuration itself
- ❌ Compromise of the provisioning process
- ❌ Quantum computer attacks on secp256k1
- ✅ Market-making bots with limited exposure
- ✅ Automated DeFi strategies with whitelisted contracts
- ✅ Development and testing environments
- ✅ Educational and research projects
- ❌ High-value treasury management
- ❌ Critical infrastructure signing
- ❌ Applications requiring formal security certification
- ❌ Multi-signature wallet implementations
The device enforces policies before signing any transaction:
const policy = {
// Allowed blockchain networks
allowedChains: [1, 10, 137, 8453],
// Whitelisted recipient addresses
toWhitelist: [
'0x742d35Cc3672C1BfeE3d4D5a0e6E9C4FfBe7E8A8',
'0xA0b86a33E6417C7Ef6D7680B2d5df2aC4d5a6E1B'
],
// Allowed function selectors (first 4 bytes of call data)
functionWhitelist: [
'0xa9059cbb', // transfer(address,uint256)
'0x095ea7b3', // approve(address,uint256)
'0x23b872dd' // transferFrom(address,address,uint256)
],
// Maximum ETH value per transaction
maxValueWei: '0x16345785d8a0000', // 0.1 ETH
// Maximum gas limit
maxGasLimit: 200000,
// Maximum fee per gas (for EIP-1559)
maxFeePerGasWei: '0x2540be400', // 10 gwei
// Allow transactions with empty data to whitelisted addresses
allowEmptyDataToWhitelist: true
};Default rate limits:
- 10 requests per minute (global)
- Configurable per-client limits
- Exponential backoff on rate limit exceeded
- Circuit breaker for repeated failures
# Install dependencies
make dev-setup
# Build and flash development version
make dev-build
make flash
make monitor
# Build production version
make prod-buildcd client
npm install
npm run build
npm testThe ESP32 Remote Signer includes a comprehensive test suite covering unit tests, integration tests, and performance benchmarks.
# Install test dependencies
cd test && pip3 install -r requirements.txt
# Run all tests with emulator
./test/run_tests.sh all# Test signing with trezor-crypto integration
./test/run_tests.sh crypto
# Test specific transaction types
python3 test/test_crypto.py::TestCryptoOperations::test_eip155_signing
python3 test/test_crypto.py::TestCryptoOperations::test_eip1559_signing# Flash firmware and run tests
make flash
export ESP32_SIGNER_URL="https://192.168.1.100"
python3 test/test_crypto.py- ✅ Unit Tests: Crypto operations, key generation, signing (Unity framework)
- ✅ API Tests: HTTP endpoints, authentication, transaction signing
- ✅ Integration Tests: Complete transaction flows using QEMU emulator
- ✅ Performance Tests: Signing rate, memory usage, response times
- ✅ Security Tests: Mode enforcement, rate limiting, input validation
# Generate detailed test report
./test/run_tests.sh all --report
# View coverage report
pytest test/ --cov=. --cov-report=html
open htmlcov/index.html📖 Detailed Documentation: See Testing Guide for complete testing documentation and Quick Start for 5-minute setup.
-
Device not responding:
- Check WiFi connectivity
- Try both IP address and mDNS hostname (e.g.,
device.local) - Verify device is in correct mode (provisioning vs signing)
- Ensure mDNS is enabled on your network
-
Authentication failures:
- Verify auth key is correct 256-bit hex string
- Check system time synchronization
- Ensure nonce is from recent /health call
-
Policy violations:
- Check transaction parameters against policy
- Verify recipient address is whitelisted
- Check function selector if sending contract calls
-
Rate limiting:
- Implement exponential backoff in client
- Reduce request frequency
- Use circuit breaker pattern
-
HTTPS certificate errors:
- Regenerate certificates:
cd main/certs && openssl req -newkey rsa:2048 -nodes -keyout server_key.pem -x509 -days 3650 -out server_cert.pem -subj "/CN=ESP32RemoteSigner" - Rebuild firmware:
make clean && make dev-build && make flash - For development, set
NODE_TLS_REJECT_UNAUTHORIZED=0to accept self-signed certs
- Regenerate certificates:
-
mDNS hostname not resolving:
- Verify hostname was set during provisioning
- Check that device is in signing mode (mDNS disabled in provisioning mode)
- Ensure your network supports mDNS/Bonjour
- Try device IP address as fallback
- On Windows, install Bonjour service or use IP address
Enable verbose logging:
export DEBUG=esp32-signer:*
node your-script.js- Fork the repository
- Create a feature branch
- Make your changes
- Add tests
- Submit a pull request
MIT License - see LICENSE file for details.
This software is provided "as is" without warranty. Use at your own risk. The authors are not responsible for any loss of funds or security breaches resulting from the use of this software.