Development of Private Key Management System
Private key is the only source of truth in blockchain. No password recovery, no support service, no transaction rollback. Key compromise = permanent loss of all assets under its control. Yet most organizations store keys in .env files on servers or, worse, in developers' personal wallets.
Developing corporate key management system is engineering task at intersection of cryptography, distributed systems, and operational security. Correct architecture must ensure: impossibility of compromise from single point of failure, auditability of every key use, recovery on component failure.
Threat model: what we protect against
Before choosing technologies — clearly define threats.
External attacker: server breach, credential leak, SQL injection in adjacent systems. Attacker gains access to execution environment.
Malicious insider: employee with legitimate access tries to use key without authorization or steal it.
Infrastructure failure: server with key fails at worst moment. Need replication without security reduction.
Supply chain attack: compromised dependency, modified Docker image, BGP hijack of cloud provider.
Different threats require different protective measures. No single right solution — toolkit with different security/convenience trade-offs.
Key protection levels
Hardware Security Modules (HSM)
HSM (Hardware Security Module) — physical device storing key material and performing cryptographic operations in protected environment. Key never leaves device in plain form.
Cloud variants: AWS CloudHSM, Azure Dedicated HSM, Google Cloud HSM. On-premise: Thales Luna, Entrust nShield. For blockchain often use YubiHSM 2 (accessible option, ~$600).
# HSM interaction via PKCS#11 (standard interface)
import pkcs11
from pkcs11 import Mechanism, KeyType, ObjectClass
def sign_ethereum_transaction_with_hsm(
tx_hash: bytes,
slot_id: int,
pin: str,
key_label: str
) -> tuple[int, int, int]:
"""
Signs transaction hash with private key inside HSM
Returns (v, r, s) signature components
"""
lib = pkcs11.lib('/usr/lib/softhsm/libsofthsm2.so') # or real HSM path
token = lib.get_token(slot_id=slot_id)
with token.open(user_pin=pin) as session:
# Find key by label
private_key = session.get_key(
object_class=ObjectClass.PRIVATE_KEY,
key_type=KeyType.EC,
label=key_label
)
# Signing happens inside HSM, key not exported
signature = private_key.sign(tx_hash, mechanism=Mechanism.ECDSA)
# Convert DER signature to (r, s)
r, s = decode_ecdsa_signature(signature)
v = determine_recovery_id(tx_hash, r, s)
return v, r, s
def generate_key_in_hsm(session, label: str) -> None:
"""Generates key pair inside HSM"""
# Key generated inside HSM and never exits in plain form
pub, priv = session.generate_keypair(
KeyType.EC,
key_length=256,
curve='secp256k1',
public_template={
pkcs11.Attribute.LABEL: label,
pkcs11.Attribute.TOKEN: True,
pkcs11.Attribute.VERIFY: True,
},
private_template={
pkcs11.Attribute.LABEL: label,
pkcs11.Attribute.TOKEN: True, # Save on token
pkcs11.Attribute.PRIVATE: True,
pkcs11.Attribute.SENSITIVE: True, # Don't export in plain form
pkcs11.Attribute.SIGN: True,
pkcs11.Attribute.EXTRACTABLE: False, # Critical: forbid export
}
)
Multi-Party Computation (MPC)
MPC — technology where key never exists in complete form on single device. Multiple participants hold shards (key shares), transaction signing happens via cryptographic protocol without assembling full key.
Used in Fireblocks, Zengo, Qredo, Coinbase Prime. Standard protocols: GG18/GG20 (Lindell et al.), FROST (Flexible Round-Optimized Schnorr Threshold Signatures).
MPC signing scheme (simplified):
Participant A has: key_share_A
Participant B has: key_share_B
Participant C has: key_share_C
For signing need any 2 of 3 (2-of-3 scheme):
1. A and B start protocol
2. Exchange commitments (don't reveal shards)
3. Compute partial signatures
4. Aggregate: signature = combine(partial_A, partial_B)
5. Result — valid ECDSA signature
6. key_share_A and key_share_B NEVER met on one device
MPC advantages over on-chain multisig: signature looks like regular single-sig transaction (less gas, doesn't reveal governance structure), off-chain policy enforcement (can add any rules without changing smart contract).
Threshold Signatures vs Multisig
Important not to confuse MPC threshold signatures with on-chain multisig (Gnosis Safe). Different approaches with different trade-offs:
| Characteristic | MPC (TSS) | On-chain Multisig (Gnosis Safe) |
|---|---|---|
| Gas cost | Standard signature | Depends on scheme (higher) |
| Transparency | Governance structure not visible | All signers public on-chain |
| Off-chain policy | Full flexibility | No |
| Signing audit | Off-chain log | On-chain history |
| Shard recovery | More complex | Easy (add/remove owner) |
| Maturity | Relatively new | Proven for years |
For most organizations: Gnosis Safe for large cold storage (transparency more important than gas), MPC for operational hot wallets (speed and policy flexibility).
Transaction authorization policies
Keys are only part of system. Equally important: define who, when, and under what conditions can sign transactions.
Policy Engine
interface TransactionPolicy {
id: string;
name: string;
conditions: PolicyCondition[];
requiredApprovals: number;
approvers: string[];
maxAmountUsd?: number;
allowedContracts?: string[];
allowedChains?: number[];
timeRestrictions?: TimeRestriction;
}
interface PolicyCondition {
type: 'amount' | 'contract' | 'method' | 'time' | 'chain';
operator: 'eq' | 'lt' | 'gt' | 'in' | 'not_in';
value: unknown;
}
class PolicyEngine {
private policies: TransactionPolicy[];
async evaluateTransaction(tx: PendingTransaction): Promise<PolicyResult> {
const matchingPolicies = this.findMatchingPolicies(tx);
if (matchingPolicies.length === 0) {
return {
allowed: false,
reason: 'No matching policy',
requiresManualReview: true
};
}
// Take most strict applicable policy
const strictestPolicy = this.getMostRestrictivePolicy(matchingPolicies);
// Check limits
if (strictestPolicy.maxAmountUsd) {
const txValueUsd = await this.getTransactionValueUsd(tx);
if (txValueUsd > strictestPolicy.maxAmountUsd) {
return {
allowed: false,
reason: `Exceeds limit: $${txValueUsd} > $${strictestPolicy.maxAmountUsd}`,
requiresApproval: true,
approvers: strictestPolicy.approvers
};
}
}
// Check contract whitelist
if (strictestPolicy.allowedContracts &&
tx.to &&
!strictestPolicy.allowedContracts.includes(tx.to.toLowerCase())) {
return {
allowed: false,
reason: 'Contract not in whitelist',
requiresManualReview: true
};
}
return { allowed: true, policy: strictestPolicy };
}
}
Authorization levels
Typical corporate hierarchy:
Automatic transactions (up to $1,000 equivalent): executed without human confirmation. Suitable for gas top-ups, small operations, batch transactions.
One-person approval ($1,000–$50,000): operation requires confirmation from one authorized person via mobile app or hardware key.
Multi-person approval ($50,000–$500,000): requires M of N confirmations from different employees in different geolocations.
Board-level approval (above $500,000): committee meeting, physical meeting, possibly notarized documents.
Key Lifecycle Management
Key lifecycle: generation → registration → operational use → rotation → revocation. Each stage requires separate procedures.
Generation: must happen in trusted environment (HSM, air-gapped machine). Entropy source verified. Generation documented with witness signatures.
Backup sharding: keys backed up via Shamir's Secret Sharing. 3-of-5 scheme: five shards distributed to different physical locations, three sufficient for recovery.
from secretsharing import PlaintextToHexSecretSharer
def backup_private_key(private_key_hex: str, shares: int = 5, threshold: int = 3) -> list[str]:
"""
Splits private key into N shards, threshold sufficient for recovery
Shards stored separately: different people, different physical locations
"""
shards = PlaintextToHexSecretSharer.split_secret(
private_key_hex,
threshold,
shares
)
return shards
def recover_private_key(shards: list[str]) -> str:
"""Recovers key from any threshold shards"""
return PlaintextToHexSecretSharer.recover_secret(shards)
# Backup procedure:
# 1. Air-gapped machine, no network
# 2. Generate 5 shards
# 3. Each shard on separate hardware drive + paper backup
# 4. Envelopes sealed, signed by witnesses
# 5. Stored in different safes (office, bank box, home safe of key employees)
Key rotation: planned (once per year) and unplanned (on compromise suspicion, employee termination). For on-chain contracts requires owner change via multisig.
Revocation: on compromise — immediate asset transfer to new key. Can't just "block" key on blockchain.
Audit and monitoring
Every key use must be logged: who requested signing, what was signed, who authorized, timestamp, IP, device fingerprint.
interface KeyUsageEvent {
eventId: string;
timestamp: Date;
keyId: string;
operation: 'sign' | 'derive' | 'export_public' | 'rotate';
requestedBy: string; // user ID or service account
approvedBy: string[]; // if approval required
transactionHash?: string; // if transaction
transactionData?: {
to: string;
value: string;
chainId: number;
methodSignature?: string;
};
policyId: string;
ipAddress: string;
deviceId: string;
approved: boolean;
rejectionReason?: string;
}
// All events signed by audit HSM key — can't forge
// Stored in append-only storage (Kafka, CloudTrail, immutable S3 bucket)
Anomalies for alerting: signing at night outside work hours, atypical destination addresses, transaction volume above historical norm, attempts to sign rejected transactions.
Disaster Recovery
Recovery plan must be documented and regularly tested (tabletop exercises). Scenarios: loss of one server with key, loss of data center, shard compromise, key keeper termination.
RTO (Recovery Time Objective) for different levels: hot wallet — minutes (automatic failover to backup HSM), cold storage — hours (multi-person procedure), full key switch — 24–48 hours.
Regular tests: quarterly recovery simulation in staging. Without test, recovery plan is just document.
Stack and development timeline
Infrastructure: AWS CloudHSM or YubiHSM 2, Hashicorp Vault (secret and policy management), Kubernetes for signing services.
MPC libraries: tss-lib (Go, GG20 implementation), ZenGo-X/multi-party-ecdsa (Rust), Fireblocks SDK for enterprise.
Backend: Go or Rust for performance-critical signing service, Node.js/Python for API gateway.
Audit: security audit of key management system mandatory, preferably with penetration testing.
Development timeline for corporate KMS: 4–6 months for production-grade system with HSM, MPC, and policy engine. Integration with Gnosis Safe or Fireblocks instead of custom MPC halves timeline.







