Warning — demo infrastructure, not audited.
This repository is reference code for learning and testnet experimentation. It has not undergone a security audit and is not production-hardened. Review IAM policies, logging, key rotation, monitoring, and threat modeling before any mainnet use.
AWS KMS/Lambda-based signing infrastructure adapted for Binance Smart Chain (BSC), focused on secure Web3 backend transaction workflows.
The core goal: avoid exposing raw private keys in application services. Your backend builds unsigned transactions and delegates signing to an isolated Lambda function that calls AWS KMS (ECC_SECG_P256K1). The private key material never appears in environment variables, request payloads, or application memory on the backend.
This repository extends the upstream AWS Ethereum sample for BSC. It is not a passive fork: it adapts chain configuration, transaction assembly, and security documentation for Binance Smart Chain backend signing workflows. See AWS-README.md for the original Ethereum deep dive.
Upstream: aws-samples/aws-kms-ethereum-accounts.
- Added BSC / Chapel chain configuration (
ETH_NETWORK,CHAIN_ID, mainnet56/ testnet97). - Adapted legacy transaction assembly for the BSC gas model (
gas+gasPrice, no EIP-1559 type-2). - Adjusted EIP-155 signing and
chainIdhandling for BSC replay protection. - Added demo examples and tests around BSC signing (
examples/,tests/test_signing_core.py). - Documented threat model and security assumptions for backend signing workflows.
- Removed the legacy
privateKey/SKEYSpayload pattern in favor of KMS-only signing.
- Not a managed custody solution.
- Not audited.
- Not a wallet.
- Not intended to bypass operational controls.
- Not production-ready without IAM hardening, monitoring, and key lifecycle management.
git clone <this-repo> && cd aws_kms_lambda_bsc
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements-dev.txt
pytest -vExpected: 11 passed in tests/test_signing_core.py (EIP-2, EIP-155, chain id resolution, tx validation, secret-field rejection).
sequenceDiagram
participant Backend as Application backend
participant Lambda as Lambda signer
participant KMS as AWS KMS (CMK)
participant RPC as BSC RPC node
Backend->>Lambda: unsigned tx (to, nonce, gas, gasPrice, data, value)
Lambda->>KMS: GetPublicKey
KMS-->>Lambda: DER public key
Lambda->>Lambda: derive BSC address, hash unsigned tx
Lambda->>KMS: Sign(digest)
KMS-->>Lambda: ECDSA (r, s)
Lambda->>Lambda: recover v, assemble EIP-155 signed tx
Lambda-->>Backend: signed_tx (rawTransaction, hash, r, s, v)
Backend->>RPC: eth_sendRawTransaction (optional)
| Component | Responsibility |
|---|---|
| Backend | Business logic, nonce management, tx assembly — no private keys |
| Lambda signer | Hash unsigned tx, orchestrate KMS signing, apply EIP-155 v |
| AWS KMS CMK | Holds secp256k1 key pair; only Sign / GetPublicKey exposed to Lambda |
| BSC RPC | Broadcast signed raw transactions (testnet recommended for demos) |
The upstream AWS sample targets Ethereum. This fork keeps the KMS signing model and adjusts chain-specific transaction parameters:
| Topic | Ethereum (typical) | BSC (this repo) |
|---|---|---|
| chainId | 1 mainnet, 5 Goerli (historical), 11155111 Sepolia |
56 mainnet, 97 Chapel testnet |
| Gas model | Often EIP-1559 (maxFeePerGas, maxPriorityFeePerGas) on L1 |
Legacy gas + gasPrice (PoSA chain) |
| Tx assembly | Type-2 fields for EIP-1559 txs | Legacy RLP encoding via serializable_unsigned_transaction_from_dict |
| Address derivation | Keccak-256 of KMS public key (same secp256k1 curve) | Identical — BSC uses the same account model as Ethereum |
| Replay protection | EIP-155: v = recovery + chainId * 2 + 35 |
Same formula with BSC chainId |
Configure the network at deploy time with ETH_NETWORK (bsc, bsc-testnet, chapel) or override with CHAIN_ID.
| Threat | Mitigation in this design | Residual risk |
|---|---|---|
| Private key theft from backend | Backend never holds or transmits key material | Compromised backend can still request arbitrary signatures if it can invoke Lambda |
| Key exfiltration from Lambda | Key stays in KMS HSM; Lambda only receives signatures | Lambda code bugs or excessive IAM permissions could weaken isolation |
| Unauthorized signing | Restrict lambda:InvokeFunction and kms:Sign via IAM |
Misconfigured resource policies are a common failure mode |
| Replay across chains | EIP-155 embeds chainId in signature |
Wrong CHAIN_ID env var could produce valid but unintended-chain txs |
| Log leakage | Lambda logs event keys only; default LOG_LEVEL=WARNING |
Raising log level or custom handlers may still leak payloads |
| Insider abuse | KMS CloudTrail + Lambda invocation auditing | Requires operational monitoring not included here |
- KMS is the sole custodian of the signing key (
ECC_SECG_P256K1,SIGN_VERIFY). - Only the signer Lambda has
kms:Signandkms:GetPublicKeyon the CMK. - Backend callers are authenticated (IAM, API Gateway authorizer, VPC, etc.) — not fully implemented in this demo.
- Transaction intent is validated off-chain before invocation (recipient, value, contract allowlists).
- Legacy gas transactions (
gas+gasPrice) are assumed; EIP-1559 type-2 txs are out of scope. - BSC PoSA consensus and RPC endpoints are trusted for broadcasting only; signing does not depend on RPC.
- Operators accept demo-grade error handling, observability, and key lifecycle practices.
Prerequisites: AWS account, CDK CLI, Python 3.9+, Node.js.
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cdk bootstrap # once per account/region
cdk deployCDK provisions:
- KMS CMK (
ECC_SECG_P256K1) - Python 3.9 Lambda with
KMS_KEY_IDandETH_NETWORKenvironment variables - IAM grants: Lambda →
kms:GetPublicKey,kms:Sign
Destroy when finished:
cdk destroyFor testnet demos, deploy with Chapel:
# app.py — pass eth_network when instantiating the stack
AwsKmsLambdaEthereumStack(app, "aws-kms-lambda-ethereum", eth_network="bsc-testnet")Use BSC Chapel testnet (chainId 97) for all local and CI experiments.
- Deploy the stack (prefer
eth_network="bsc-testnet"). - Read the KMS-derived address from Lambda logs or by invoking with a dry-run helper.
- Fund the address via a Chapel faucet.
- Copy
.env.example→.envand setAWS_LAMBDA_FUNCTION_NAMEandAWS_REGION. - Run the example client:
pip install -r requirements-dev.txt
python examples/lambda_signer_client.py- Broadcast
signed_tx.rawTransactionwitheth_sendRawTransactionagainst a Chapel RPC (BSC_RPC_URLin.env.example).
Do not use this setup for mainnet funds without a full security review.
The backend sends only unsigned transaction fields. Secret key material (privateKey, mnemonics, seeds, SKEYS) must never appear in payloads — the signer rejects them on both client and Lambda.
Lambda invoke payload (unsigned tx only):
{
"to": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0",
"nonce": 3,
"gas": 21000,
"gasPrice": 10000000000,
"value": 0,
"data": "0x"
}No privateKey. No chainId from the caller — it is set server-side from ETH_NETWORK / CHAIN_ID.
Lambda response (sanitized):
{
"signed_tx": {
"rawTransaction": "0xf86c...a1b2",
"hash": "0x9c3e...4f80",
"r": 1234567890123456789012345678901234567890123456789012345678901234,
"s": 9876543210987654321098765432109876543210987654321098765432109,
"v": 230,
"from": "0xAb12...cD34",
"chainId": 97
}
}Hex values are truncated for documentation. Use rawTransaction with eth_sendRawTransaction on Chapel testnet.
import boto3, json, os
client = boto3.client("lambda", region_name=os.getenv("AWS_REGION"))
payload = {
"to": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0",
"nonce": 0,
"gas": 21000,
"gasPrice": 10_000_000_000,
"value": 0,
"data": "0x",
}
response = client.invoke(
FunctionName=os.environ["AWS_LAMBDA_FUNCTION_NAME"],
InvocationType="RequestResponse",
Payload=json.dumps(payload),
)
result = json.load(response["Payload"])
signed = result["signed_tx"]A fuller client with guardrails lives in examples/lambda_signer_client.py.
| Field | Required | Description |
|---|---|---|
to |
yes | Recipient address |
nonce |
yes | Account nonce |
gas |
yes | Gas limit |
gasPrice |
yes | Legacy gas price in wei |
data |
yes | Call data (0x for transfers) |
value |
no | Wei to send (default 0) |
chainId is taken from the Lambda environment (ETH_NETWORK / CHAIN_ID), not from the caller — preventing cross-chain confusion from untrusted input.
See .env.example. Lambda-side variables are set by CDK:
| Variable | Set by | Purpose |
|---|---|---|
KMS_KEY_ID |
CDK | CMK used for signing |
ETH_NETWORK |
CDK | bsc, bsc-testnet, or chapel |
CHAIN_ID |
optional override | Explicit EIP-155 chain id |
LOG_LEVEL |
CDK / ops | Logging verbosity |
Run the full unit suite:
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements-dev.txt
pytest -v
# or: pytest tests/test_signing_core.py -vCoverage in tests/test_signing_core.py:
- EIP-2 low-
snormalization - EIP-155
vcalculation (BSC mainnet / Chapel testnet) - Chain id resolution (
bsc,bsc-testnet,chapel,CHAIN_IDoverride) - Tx parameter validation (required fields, no secret fields)
- Rejection of
privateKey/SKEYSin incoming events
Lambda integration tests require Python 3.9 and the eth_client dependencies bundled at deploy time.
app.py # CDK entrypoint
aws_kms_lambda_ethereum/
aws_kms_lambda_ethereum_stack.py
_lambda/functions/eth_client/
lambda_function.py # handler
lambda_helper.py # KMS signing orchestration
signing_core.py # Pure helpers (chain id, EIP-155, tx params)
examples/
lambda_signer_client.py # backend client (no private keys)
tests/
test_signing_core.py
- AWS Blog — KMS Ethereum accounts (Part 1)
- AWS Blog — KMS Ethereum accounts (Part 2)
- BSC Chapel testnet
MIT-0 — see LICENSE.