DEV Community

Heartlin Machado
Heartlin Machado

Posted on

Add Pre-Signature Policy Enforcement to AWS KMS Signing in 10 Minutes

If you're using AWS KMS for blockchain transaction signing, every call to kms:Sign is an unrestricted operation. Any process with the right IAM permissions can sign any transaction to any address for any amount.

This tutorial shows you how to add a policy evaluation layer in front of your KMS signing operations — so every transaction is checked against rules (value limits, address allowlists, velocity caps) before the signature is produced. If the policy says no, the key never signs.

We'll use Gate, an open-source pre-signature enforcement SDK, in SHADOW mode — meaning it evaluates every transaction and logs the decision, but never blocks. Zero risk to your existing operations.


What you'll build

Your App → Gate SDK (evaluate) → Policy Decision → KMS Sign (only if ALLOW)
                                      ↓
                            Signed receipt (HMAC + RSA)
                            Decision log (DynamoDB)
Enter fullscreen mode Exit fullscreen mode

Every signing request gets:

  • A policy evaluation (denylist, value threshold, velocity, signer authorization)
  • A cryptographically signed receipt (verifiable by third parties)
  • A decision log entry (queryable audit trail)

In SHADOW mode, the SDK never blocks — it returns ALLOW for everything but logs WOULD_BLOCK when policy violations are detected. You see what Gate would catch without any production risk.


Prerequisites

  • Node.js 18+
  • An AWS account with a KMS asymmetric signing key (secp256k1 or ECDSA P-256)
  • A Gate tenant (free trial at blockintelai.com)

Step 1: Install

npm install blockintel-gate-sdk @aws-sdk/client-kms uuid
Enter fullscreen mode Exit fullscreen mode

Step 2: Initialize the Gate client

import { createGateClient } from 'blockintel-gate-sdk';

const gate = createGateClient({
  apiUrl: 'https://gate-hotpath.blockintelai.com',
  tenantId: process.env.GATE_TENANT_ID,
  apiKey: process.env.GATE_API_KEY,
  environment: 'production',
  signerId: 'treasury-signer-1',
  onConnectionFailure: 'FAIL_OPEN',  // SHADOW mode default — never block
});
Enter fullscreen mode Exit fullscreen mode

FAIL_OPEN means that if Gate itself is unreachable, your signing operations continue uninterrupted. Gate is never a Tier-0 dependency unless you want it to be.

Step 3: Wrap your signing function

Here's a typical KMS signing function without Gate:

import { KMSClient, SignCommand } from '@aws-sdk/client-kms';
import { v4 as uuidv4 } from 'uuid';

const kms = new KMSClient({ region: 'us-east-1' });

// ❌ Before: unrestricted signing
async function signTransaction(txHash: Buffer, keyId: string) {
  const result = await kms.send(new SignCommand({
    KeyId: keyId,
    Message: txHash,
    MessageType: 'DIGEST',
    SigningAlgorithm: 'ECDSA_SHA_256',
  }));
  return result.Signature;
}
Enter fullscreen mode Exit fullscreen mode

Here's the same function with Gate evaluation:

// ✅ After: every signing operation evaluated against policy
async function signTransactionWithGate(
  txHash: Buffer,
  keyId: string,
  toAddress: string,
  valueWei: string,
  valueUsd: number,
) {
  // Step 1: Ask Gate if this transaction should proceed
  const decision = await gate.evaluate({
    requestId: uuidv4(),
    txIntent: {
      toAddress,
      value: valueWei,
      valueUsd,
      network: 'ethereum',
      networkFamily: 'EVM',
      chainId: 1,
      from: '0xYourWalletAddress',
    },
    signingContext: {
      signerId: 'treasury-signer-1',
      actorPrincipal: 'ops@yourcompany.com',
    },
  });

  // Step 2: In SHADOW mode, decision is always ALLOW
  // But decision.reasonCodes tells you what WOULD have been blocked
  if (decision.reasonCodes?.length > 0) {
    console.warn(`[GATE] Would block: ${decision.reasonCodes.join(', ')}`);
  }

  // Step 3: Receipt is signed and logged regardless
  console.log(`[GATE] Receipt: ${decision.receiptSignature}`);

  // Step 4: Sign with KMS (always proceeds in SHADOW mode)
  const result = await kms.send(new SignCommand({
    KeyId: keyId,
    Message: txHash,
    MessageType: 'DIGEST',
    SigningAlgorithm: 'ECDSA_SHA_256',
  }));

  return {
    signature: result.Signature,
    gateDecision: decision.decision,
    gateReceipt: decision.receiptSignature,
    wouldBlock: decision.reasonCodes,
  };
}
Enter fullscreen mode Exit fullscreen mode

That's it. Every KMS signing operation now produces a policy decision and a signed receipt. In SHADOW mode, nothing is blocked — you're collecting data.

Step 4: Set up policies

Log into the Gate Console and create policies for your tenant. Common starting rules:

Rule What it does Example
Denylist Blocks transactions to known malicious addresses OFAC SDN list, known exploit contracts
Destination Allowlist Only allow transfers to approved addresses Your hot wallets, exchange deposit addresses
Value Threshold Block transactions above a USD amount Max $100,000 per transaction
Velocity Limit Cap transaction volume in a rolling window Max $500,000 per 10 minutes
Signer Allowlist Only approved signing identities can request treasury-signer-1, ops-signer-2
New Destination Flag first-time destination addresses Log or require step-up approval

After creating rules, compile and publish a policy snapshot. The hot path loads the snapshot and evaluates every gate.evaluate() call against it.

Step 5: Verify it works

// Test: normal transaction (should be ALLOW)
const normalTx = await signTransactionWithGate(
  Buffer.from('abc123', 'hex'),
  'arn:aws:kms:us-east-1:123456789:key/your-key-id',
  '0x742d35Cc6634c0532925a3b844Bc9e7595916DA2',  // Known good address
  '1000000000000000000',  // 1 ETH
  2500,  // $2,500
);
console.log(normalTx.gateDecision);   // "ALLOW"
console.log(normalTx.wouldBlock);     // []

// Test: denylisted address (should be WOULD_BLOCK in SHADOW)
const blockedTx = await signTransactionWithGate(
  Buffer.from('def456', 'hex'),
  'arn:aws:kms:us-east-1:123456789:key/your-key-id',
  '0xba5ed7f51d2a0cfeef2ca243af1188bad4f1cb03',  // Known exploit address
  '50000000000000000000',  // 50 ETH
  125000,  // $125,000
);
console.log(blockedTx.gateDecision);  // "ALLOW" (SHADOW mode override)
console.log(blockedTx.wouldBlock);    // ["DENYLIST"]
Enter fullscreen mode Exit fullscreen mode

In SHADOW mode, both transactions succeed. But the second one logs WOULD_BLOCK with reason DENYLIST. You now have visibility into what Gate would catch — without changing your production behavior.


What you get after 14 days

Let Gate run in SHADOW mode for two weeks. You'll accumulate:

  • A decision log — every signing operation evaluated, with policy verdicts
  • A block rate — what percentage of transactions would have been blocked by your policy
  • A max-loss estimate — if a signing key were compromised, how much could an attacker drain in 24 hours under your current policy (this is the Bot Blast Radius metric)
  • Signed receipts — cryptographically signed records of every decision, verifiable by third parties (useful for insurance, compliance, audits)

This data is visible in the Gate Console dashboard. If you're working with a custody insurance carrier, the BBR report and evidence bundle are exactly what underwriters need to evaluate your signing controls.


Going from SHADOW to enforcement

When you're ready to enforce (not just monitor), the transition is one config change:

const gate = createGateClient({
  // ... same config
  onConnectionFailure: 'FAIL_CLOSED',  // Now: block on policy violation
});
Enter fullscreen mode Exit fullscreen mode

At SOFT_ENFORCE, the SDK throws BlockIntelBlockedError when a policy violation is detected. Your code catches it and decides what to do.

At HARD_KMS_GATEWAY, Gate removes kms:Sign from your application's IAM role entirely. The only path to a signature is through Gate's policy evaluation. Even a fully compromised application cannot sign a transaction that violates policy — because the application no longer has the IAM permission to call KMS directly.

That's the enforcement ladder: SHADOW (monitor) → SOFT_ENFORCE (SDK blocks) → HARD_KMS_GATEWAY (IAM blocks). Each step is reversible. Each step produces more evidence.


Python

The same flow works with the Python SDK:

pip install gate-sdk boto3
Enter fullscreen mode Exit fullscreen mode
from gate_sdk import GateClient

gate = GateClient(
    base_url="https://gate-hotpath.blockintelai.com",
    tenant_id=os.environ["GATE_TENANT_ID"],
    api_key=os.environ["GATE_API_KEY"],
    environment="production",
    signer_id="treasury-signer-1",
)

decision = gate.evaluate(
    request_id=str(uuid4()),
    to_address="0x742d35Cc6634c0532925a3b844Bc9e7595916DA2",
    value="1000000000000000000",
    value_usd=2500,
    network="ethereum",
    network_family="EVM",
    chain_id=1,
    from_address="0xYourWalletAddress",
    signer_id="treasury-signer-1",
    actor_principal="ops@yourcompany.com",
)

print(f"Decision: {decision.decision}")
print(f"Receipt: {decision.receipt_signature}")
Enter fullscreen mode Exit fullscreen mode

Note: the Python SDK's default timeout is 50ms (vs 15s in TypeScript) — it's optimized for hot-path latency. If you see circuit breaker trips, check your control plane p99 before increasing the timeout.


Links


Built by BlockIntel. Gate is pre-signature policy enforcement for crypto custody — we stop unauthorized transactions before they're signed, not after.

Top comments (0)