@l8d/hardhat-yubikey
v1.0.7
Published
Hardhat v3 plugin for YubiKey hardware wallet using OpenPGP card
Maintainers
Readme
@l8d/hardhat-yubikey
A Hardhat v3 plugin that enables integration with YubiKey hardware wallets using the OpenPGP card application with secp256k1 keys for Ethereum transaction signing.
Features
- 🔐 Sign Ethereum transactions using YubiKey's OpenPGP card application
- 🔑 Support for secp256k1 elliptic curve (Ethereum-compatible)
- 🎯 Multiple OpenPGP key slots (signature, decryption, authentication)
- ⚡ Compatible with Hardhat v3 plugin system
- 🛡️ Hardware-backed private key security
- 📝 Support for all Ethereum signing methods (transactions, messages, EIP-712 typed data)
- 🔄 EIP-1559 and legacy transaction support
Requirements
- YubiKey firmware 5.2.3+ with OpenPGP card application
- secp256k1 keys configured in OpenPGP card
- PC/SC smart card daemon (pcscd) installed and running
- Node.js and Hardhat v3
Installation
npm install @l8d/hardhat-yubikeySystem Dependencies
macOS
# PC/SC is built-in, but you may need build tools for native modules
xcode-select --installLinux
# Debian/Ubuntu
sudo apt-get install libpcsclite1 libpcsclite-dev pcscd
# Fedora/RHEL
sudo dnf install pcsc-lite pcsc-lite-devel pcsc-lite-ccid
# Arch Linux
sudo pacman -S pcsclite ccid
# Start pcscd service
sudo systemctl start pcscd
sudo systemctl enable pcscdWindows
- Install PC/SC smart card drivers
- Windows has built-in PC/SC support
YubiKey Setup
Before using this plugin, you need to configure your YubiKey's OpenPGP card with secp256k1 keys:
1. Install YubiKey Manager
# macOS
brew install ykman
# Linux
pip install yubikey-manager
# Windows
# Download from https://www.yubico.com/support/download/yubikey-manager/2. Generate secp256k1 Keys
# Check YubiKey info and firmware version (must be 5.2.3+)
ykman openpgp info
# Generate a secp256k1 signature key in slot 1 (SIGNATURE)
gpg --card-edit
> admin
> generate
# Select secp256k1 curve when promptedAlternatively, you can import existing secp256k1 keys:
# Import private key to signature slot
gpg --expert --edit-key YOUR_KEY_ID
> keytocard
# Select slot 1 (Signature key)3. Get Your Ethereum Address
To derive your Ethereum address from the YubiKey's secp256k1 public key:
# Export public key
gpg --export YOUR_KEY_ID > pubkey.gpg
# Use a tool to convert GPG public key to Ethereum address
# (or use the address you derived when generating the key)Usage
Basic Configuration
Add the plugin to your hardhat.config.ts:
import { defineConfig } from "hardhat/config";
import "@l8d/hardhat-yubikey";
export default defineConfig({
solidity: "0.8.19",
networks: {
sepolia: {
url: "https://sepolia.infura.io/v3/YOUR-PROJECT-ID",
yubikeyAccounts: [
{
address: "0xYOUR_ETHEREUM_ADDRESS",
slot: "signature", // or OpenPGPSlot.SIGNATURE (0x00)
},
],
yubikeyOptions: {
connectionTimeout: 30000, // 30 seconds (default)
requireConfirmation: true, // Require touch for each signature
},
},
},
});Slot Options
The plugin supports three OpenPGP card key slots:
yubikeyAccounts: [
{ address: "0x...", slot: "signature" }, // Slot 1 (0x00)
{ address: "0x...", slot: "decryption" }, // Slot 2 (0x01)
{ address: "0x...", slot: "authentication" }, // Slot 3 (0x02)
]Multiple Accounts
You can configure multiple accounts using different slots:
yubikeyAccounts: [
{ address: "0xAddress1...", slot: "signature" },
{ address: "0xAddress2...", slot: "authentication" },
]Deploying Contracts
// scripts/deploy.ts
import { ethers, network } from "hardhat";
async function main() {
// Make sure YubiKey is connected and unlocked
await network.provider.request({
method: "eth_requestAccounts",
params: [],
});
const [deployer] = await ethers.getSigners();
console.log("Deploying with account:", deployer.address);
// Plugin will prompt for YubiKey PIN and touch confirmation
const MyContract = await ethers.getContractFactory("MyContract");
const contract = await MyContract.deploy();
await contract.waitForDeployment();
console.log("Contract deployed to:", await contract.getAddress());
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});Signing Messages
const signer = await ethers.getSigner("0xYOUR_YUBIKEY_ADDRESS");
// Sign personal message
const message = "Hello, Ethereum!";
const signature = await signer.signMessage(message);
// Sign EIP-712 typed data
const domain = { name: "MyApp", version: "1", chainId: 1 };
const types = { Message: [{ name: "content", type: "string" }] };
const value = { content: "Hello!" };
const typedSignature = await signer.signTypedData(domain, types, value);Configuration Options
yubikeyAccounts
Array of account configurations, each containing:
address(string, required): Ethereum address (0x-prefixed)slot(string | number, required): OpenPGP key slot- String values:
"signature","decryption","authentication" - Number values:
0x00,0x01,0x02
- String values:
yubikeyOptions
Optional configuration object:
connectionTimeout(number): Timeout in milliseconds for YubiKey connection (default: 30000)requireConfirmation(boolean): Require physical touch for signatures (default: true)
How It Works
- Connection: Plugin uses PC/SC to communicate with YubiKey's OpenPGP card application
- Key Selection: Maps Ethereum addresses to specific OpenPGP key slots
- Signing: When a transaction needs signing:
- Hashes the transaction using Keccak256
- Sends COMPUTE SIGNATURE command to YubiKey
- YubiKey returns ECDSA signature (r, s)
- Plugin recovers the v value and formats signature for Ethereum
- Confirmation: Physical touch required for each signature (if enabled)
Security Considerations
- PIN Protection: OpenPGP card requires PIN entry before signing
- Touch Policy: Configure YubiKey to require touch for signatures
- Private Key: Never leaves the YubiKey hardware
- Slot Isolation: Keys in different slots are cryptographically isolated
- Cache Security: Slot-to-address mappings cached in
~/.cache/hardhat/yubikey/accounts.json
Troubleshooting
YubiKey Not Detected
# Check if pcscd is running
ps aux | grep pcscd
# Test YubiKey detection
pcsc_scan
# Check YubiKey status
ykman listsecp256k1 Not Supported
Ensure your YubiKey firmware is 5.2.3 or later:
ykman openpgp info
# Look for: OpenPGP version: 3.4
# Supported curves should include: secp256k1PIN Required Error
The OpenPGP card requires PIN entry. Verify your PIN:
gpg --card-status
# This will prompt for PINWrong Signature Error
- Verify the address matches the public key in the specified slot
- Check that you're using secp256k1 (not secp256r1 or other curves)
- Ensure the key was generated or imported correctly
Comparison to Ledger Plugin
| Feature | Ledger Plugin | YubiKey Plugin | |---------|--------------|----------------| | Hardware | Ledger devices | YubiKey 5.2.3+ | | Protocol | Ledger proprietary | OpenPGP card | | Key Derivation | BIP44 HD wallet paths | Fixed key slots | | Curve Support | Native secp256k1 | OpenPGP secp256k1 | | Setup Complexity | Low | Medium | | Multi-purpose | Crypto-specific | General PKI + crypto |
API Reference
Provider Methods
The plugin wraps Hardhat's provider with YubikeyProvider:
// Check connection
const isConnected = provider.connection.isConnected();
// Get YubiKey-controlled accounts
const accounts = await provider.request({ method: "eth_accounts" });
// Sign transaction
const txHash = await provider.request({
method: "eth_sendTransaction",
params: [{ from: "0x...", to: "0x...", value: "0x..." }],
});Development
# Install dependencies
npm install
# Build
npm run build
# Run tests
npm test
# Lint
npm run lintLicense
MIT
Contributing
Contributions welcome! Please open an issue or PR.
Credits
- Based on
@l8d/hardhat-ledger - Uses pcsclite for PC/SC communication
- Uses @noble/curves for secp256k1 operations
Support
- Issues: GitHub Issues
- Yubico Docs: YubiKey OpenPGP
- Hardhat Docs: Hardhat Plugins
