Building Secure Crypto Wallets with AWS KMS
The Challenge: Securing Cryptocurrency Withdrawals
When one of our clients approached us with a cryptocurrency withdrawal system challenge, they emphasized a critical requirement: developers should never have direct access to wallet private keys, especially considering these wallets would hold substantial funds. This highlighted a common problem in the crypto space: How do you securely manage private keys while maintaining operational efficiency and ensuring proper access controls?
Traditional cryptocurrency wallets face several security challenges:
Private keys stored on devices are vulnerable to theft
Hardware wallets, while secure, can be cumbersome for automated systems
Hot wallets are convenient but risky for large amounts
Cold storage is secure but impractical for regular operations
Developer access to private keys creates significant security risks
Enter AWS KMS: A Game-Changing Solution
After evaluating various options, we landed on Amazon Web Services Key Management Service (AWS KMS) as our solution. Think of AWS KMS as a super-secure digital vault that manages cryptographic keys. Instead of storing private keys on regular servers or devices, they're stored in hardware security modules (HSMs) managed by AWS.
Why AWS KMS?
Bank-Grade Security: AWS KMS uses FIPS 140-2 validated hardware security modules
No Exposed Private Keys: The private key never leaves AWS's secure infrastructure
Fine-Grained Access Control: You can control who can use the keys and how
Audit Trail: Every key usage is logged and traceable
High Availability: AWS's infrastructure ensures the system is always accessible
Developer-Production Separation: Developers can deploy and maintain the system without ever accessing the keys
Access Control: A Critical Feature
One of the most powerful aspects of using AWS KMS is its robust access control system. Here's how we structure it:
Production Environment
Only the application has permission to use (not view) the keys through IAM roles
Operations are limited to specific API actions like
Sign
andGetPublicKey
All key usage is logged in CloudTrail for audit purposes
Even administrators cannot extract or view the private key material
Development Environment
Developers work with test keys in a separate environment
Production key permissions are strictly limited to production applications
Developers can maintain the system without accessing sensitive keys
Changes to key permissions require multiple approvals
Here's an example IAM policy that demonstrates this separation:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"kms:Sign",
"kms:GetPublicKey"
],
"Resource": "arn:aws:kms:region:account:key/*",
"Condition": {
"StringEquals": {
"aws:PrincipalTag/Environment": "production"
}
}
}
]
}
How I Built It
Let's dive deep into the implementation details with comprehensive code samples for each major functionality.
1. Setting Up the Wallet Manager
First, we need to create our main wallet manager class that will handle all crypto operations:
interface WalletConfig {
region: string;
keyDescription?: string;
keyAlias?: string;
credentials?: {
accessKeyId: string;
secretAccessKey: string;
sessionToken?: string;
} | {
profile?: string;
awsCredentialsFile?: string;
};
}
interface ProviderConfig {
rpcUrl: string;
chainId: number;
}
export class CryptoWalletManager {
private readonly kmsClient: KMSClient;
private readonly provider: ethers.JsonRpcProvider;
constructor(config: WalletConfig, providerConfig: ProviderConfig) {
this.region = config.region;
this.keyDescription = config.keyDescription ?? 'Ethereum wallet key';
// Initialize provider
this.provider = new ethers.JsonRpcProvider(
providerConfig.rpcUrl,
providerConfig.chainId
);
// Initialize KMS client
const kmsConfig: Record<string, any> = {
region: this.region,
};
if (config.credentials) {
if ('accessKeyId' in config.credentials) {
kmsConfig.credentials = {
accessKeyId: config.credentials.accessKeyId,
secretAccessKey: config.credentials.secretAccessKey,
sessionToken: config.credentials.sessionToken,
};
} else {
kmsConfig.credentials = fromIni({
profile: config.credentials.profile ?? 'default',
filepath: config.credentials.awsCredentialsFile,
});
}
}
this.kmsClient = new KMSClient(kmsConfig);
}
}
2. Creating a New Wallet
Here's how we create a new Ethereum wallet using AWS KMS:
async createWallet(): Promise<WalletInfo> {
try {
// Create a new KMS key
const createKeyResponse = await this.kmsClient.send(
new CreateKeyCommand({
Description: this.keyDescription,
KeySpec: 'ECC_SECG_P256K1', // Required for Ethereum
KeyUsage: 'SIGN_VERIFY',
})
);
const keyId = createKeyResponse.KeyMetadata?.KeyId;
if (!keyId) {
throw new Error('Failed to create KMS key: KeyId is undefined');
}
// Get the public key
const publicKeyResponse = await this.kmsClient.send(
new GetPublicKeyCommand({
KeyId: keyId,
})
);
if (!publicKeyResponse.PublicKey) {
throw new Error('Failed to retrieve public key');
}
// Derive Ethereum address from public key
const address = this.getAddressFromKMSPublicKey(
publicKeyResponse.PublicKey
);
return {
address,
keyId,
};
} catch (error) {
throw new Error(`Failed to create wallet: ${error.message}`);
}
}
private getAddressFromKMSPublicKey(publicKey: Uint8Array): string {
// Extract the uncompressed public key from the DER format
const uncompressedPublicKey = new Uint8Array(publicKey).subarray(23, 23 + 65);
// Convert to hex string
const publicKeyHex = ethers.hexlify(uncompressedPublicKey);
// Compute Ethereum address
const address = ethers.computeAddress(publicKeyHex);
return address;
}
3. Creating an Ethers.js Wallet Instance
To interact with Ethereum networks, we need to create a custom signer that works with AWS KMS:
class KMSWalletSigner extends AbstractSigner {
private address: string | null = null;
constructor(
private readonly kmsClient: KMSClient,
private readonly keyId: string,
provider: any,
) {
super(provider);
}
async getAddress(): Promise<string> {
if (this.address === null) {
const publicKeyResponse = await this.kmsClient.send(
new GetPublicKeyCommand({
KeyId: this.keyId,
})
);
if (!publicKeyResponse.PublicKey) {
throw new Error('Failed to get public key from KMS');
}
// Extract the uncompressed public key
const uncompressedPublicKey = new Uint8Array(
publicKeyResponse.PublicKey
).subarray(23, 23 + 65);
const publicKeyHex = hexlify(uncompressedPublicKey);
this.address = computeAddress(publicKeyHex);
}
return this.address;
}
}
// In CryptoWalletManager class:
async getEthersWallet(kmsKeyId: string): Promise<Wallet> {
const signer = new KMSWalletSigner(this.kmsClient, kmsKeyId, this.provider);
return signer as unknown as Wallet;
}
4. Signing Transactions and Messages
The KMS signer needs to handle various types of signing operations:
// In KMSWalletSigner class:
async signMessage(message: string | Uint8Array): Promise<string> {
const messageBytes = typeof message === 'string' ? getBytes(message) : message;
const messageHash = hashMessage(messageBytes);
const signature = await this.signDigest(messageHash);
return signature.serialized;
}
async signTransaction(tx: any): Promise<string> {
const transaction = Transaction.from(tx);
transaction.signature = await this.signDigest(transaction.unsignedHash);
return transaction.serialized;
}
private async signDigest(digestHex: string): Promise<Signature> {
const digest = getBytes(digestHex);
const signResponse = await this.kmsClient.send(
new SignCommand({
KeyId: this.keyId,
Message: digest,
SigningAlgorithm: 'ECDSA_SHA_256',
MessageType: 'DIGEST',
})
);
if (!signResponse.Signature) {
throw new Error('Failed to sign with KMS');
}
// Parse the DER-encoded signature
const { r, s } = this.parseDERSignature(Buffer.from(signResponse.Signature));
// Normalize s value
const curveN = BigInt(
'0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141'
);
const halfCurveN = curveN / BigInt(2);
let normalizedS = s;
if (s > halfCurveN) {
normalizedS = curveN - s;
}
// Convert to hex strings
const rHex = toBeHex(r, 32);
const sHex = toBeHex(normalizedS, 32);
// Determine recovery id (v)
let v = 27;
const msgHash = hexlify(digest);
try {
const recoveredAddress = recoverAddress(msgHash, { r: rHex, s: sHex, v });
if (recoveredAddress.toLowerCase() !== (await this.getAddress()).toLowerCase()) {
v = 28;
}
} catch {
v = 28;
}
return Signature.from({ r: rHex, s: sHex, v });
}
5. Usage Examples
Here's how to use the wallet manager in practice:
async function example() {
// Initialize the wallet manager
const walletManager = new CryptoWalletManager(
{
region: 'us-east-1',
credentials: {
accessKeyId: 'YOUR_ACCESS_KEY',
secretAccessKey: 'YOUR_SECRET_KEY',
},
},
{
rpcUrl: 'https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY',
chainId: 1,
}
);
// Create a new wallet
const wallet = await walletManager.createWallet();
console.log('New wallet address:', wallet.address);
// Get wallet balance
const balance = await walletManager.getBalance(wallet.address);
console.log('Balance:', balance, 'ETH');
// Create ethers.js wallet instance
const ethersWallet = await walletManager.getEthersWallet(wallet.keyId);
// Sign a transaction
const tx = {
to: "0x...",
value: ethers.parseEther("0.1"),
gasLimit: "21000",
maxFeePerGas: ethers.parseUnits("20", "gwei"),
maxPriorityFeePerGas: ethers.parseUnits("1", "gwei"),
nonce: await ethersWallet.getNonce(),
type: 2,
chainId: 1
};
const signedTx = await ethersWallet.signTransaction(tx);
console.log('Signed transaction:', signedTx);
// Sign a message
const message = "Hello, Ethereum!";
const signature = await ethersWallet.signMessage(message);
console.log('Signed message:', signature);
}
Security Architecture
Our implementation follows a strict security model:
Application Layer
Only has permissions to sign transactions and get public keys
Cannot view or export private keys
Runs with minimal required permissions
Developer Access
No access to production keys
Can deploy code and maintain systems
All actions are logged and audited
Administrative Layer
Can manage key metadata and permissions
Cannot access key material
Changes require multiple approvals
Real-World Use Cases
Our solution has proven valuable in several scenarios:
Cryptocurrency Exchanges
Automated withdrawal processing
Customer deposit address generation
Treasury management
Complete separation between dev team and production keys
DeFi Applications
Smart contract interactions
Automated trading systems
Liquidity provision
Secure key management for high-value wallets
Enterprise Crypto Operations
Corporate treasury management
Payment processing systems
Cross-border transactions
Compliance with internal security policies
Benefits We've Seen
Enhanced Security
No private key exposure
Multi-layer security controls
Automated key rotation options
Complete developer-key separation
Operational Efficiency
Automated signing process
High availability
Scalable architecture
Compliance Friendly
Comprehensive audit logs
Access control policies
Integration with existing AWS security tools
Clear separation of duties
Implementation Tips
If you're considering implementing this solution, here are some key tips:
Start Small
Begin with a test wallet
Use AWS's test environment
Gradually increase transaction volumes
Security Best Practices
Implement multi-factor authentication
Use the principle of least privilege
Regular security audits
Never share production key access
Monitoring
Set up CloudWatch alerts
Monitor transaction patterns
Track key usage metrics
Alert on unauthorized access attempts
AWS KMS has proven to be a robust solution for secure cryptocurrency wallet management. It offers the perfect balance between security and usability, making it ideal for businesses handling digital assets. The ability to completely separate developer access from production keys while maintaining system functionality is a game-changer for organizations handling large cryptocurrency funds.
While implementing this solution requires careful planning and expertise, the benefits far outweigh the initial setup complexity. Our system has been running successfully in production, handling millions in transactions while maintaining enterprise-grade security standards and ensuring that private keys remain completely inaccessible to development teams.
Remember, in the world of cryptocurrency, security isn't just a feature – it's a fundamental requirement. AWS KMS helps achieve this without sacrificing operational efficiency or compromising on access controls.
Have you implemented similar solutions for cryptocurrency management? I'd love to hear about your experiences in the comments below.
Subscribe to my newsletter
Read articles from Diluk Angelo directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Diluk Angelo
Diluk Angelo
Hey there! I'm Diluk Angelo, a Tech Lead and Web3 developer passionate about bridging the gap between traditional web solutions and the decentralized future. With years of leadership experience under my belt, I've guided teams and mentored developers in their technical journey. What really drives me is the art of transformation – taking proven Web2 solutions and reimagining them for the Web3 ecosystem while ensuring they remain scalable and efficient. Through this blog, I share practical insights from my experience in architecting decentralized solutions, leading technical teams, and navigating the exciting challenges of Web3 development. Whether you're a seasoned developer looking to pivot to Web3 or a curious mind exploring the possibilities of decentralized technology, you'll find actionable knowledge and real-world perspectives here. Expect deep dives into Web3 architecture, scalability solutions, team leadership in blockchain projects, and practical guides on transitioning from Web2 to Web3. I believe in making complex concepts accessible and sharing lessons learned from the trenches. Join me as we explore the future of the web, one block at a time!