Building Secure Crypto Wallets with AWS KMS

Diluk AngeloDiluk Angelo
8 min read

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?

  1. Bank-Grade Security: AWS KMS uses FIPS 140-2 validated hardware security modules

  2. No Exposed Private Keys: The private key never leaves AWS's secure infrastructure

  3. Fine-Grained Access Control: You can control who can use the keys and how

  4. Audit Trail: Every key usage is logged and traceable

  5. High Availability: AWS's infrastructure ensures the system is always accessible

  6. 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 and GetPublicKey

  • 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:

  1. Application Layer

    • Only has permissions to sign transactions and get public keys

    • Cannot view or export private keys

    • Runs with minimal required permissions

  2. Developer Access

    • No access to production keys

    • Can deploy code and maintain systems

    • All actions are logged and audited

  3. 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:

  1. Cryptocurrency Exchanges

    • Automated withdrawal processing

    • Customer deposit address generation

    • Treasury management

    • Complete separation between dev team and production keys

  2. DeFi Applications

    • Smart contract interactions

    • Automated trading systems

    • Liquidity provision

    • Secure key management for high-value wallets

  3. Enterprise Crypto Operations

    • Corporate treasury management

    • Payment processing systems

    • Cross-border transactions

    • Compliance with internal security policies

Benefits We've Seen

  1. Enhanced Security

    • No private key exposure

    • Multi-layer security controls

    • Automated key rotation options

    • Complete developer-key separation

  2. Operational Efficiency

    • Automated signing process

    • High availability

    • Scalable architecture

  3. 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:

  1. Start Small

    • Begin with a test wallet

    • Use AWS's test environment

    • Gradually increase transaction volumes

  2. Security Best Practices

    • Implement multi-factor authentication

    • Use the principle of least privilege

    • Regular security audits

    • Never share production key access

  3. 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.

6
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!