Overview
PyHSM is a production-ready software Key Management Service (KMS) that provides:
- AES-256-GCM authenticated encryption with Argon2id key derivation
- 9 key types: AES-128/256, RSA-2048/4096, EC P-256/P-384/P-521/secp256k1, Ed25519
- Double-encrypted keystore (AES-GCM envelope + AES-KWP per-key wrapping)
- HMAC-chained tamper-evident audit logs
- Shamir M-of-N secret sharing over GF(256)
- Key rotation with version tracking and automatic decryption routing
- Caller ACLs, rate limiting, and operation caps
- Process isolation mode (Unix domain socket IPC)
- Dual implementation: Python and TypeScript with full feature parity
- Self-test Known-Answer Tests (KATs) on startup
PyHSM is MIT licensed open-source software. It requires no hardware, no cloud dependency, and no per-operation fees.
Installation
From PyPI (Recommended)
pip install vectorguard-pyhsm
From Source
git clone https://github.com/pavondunbar/PyHSM.git
cd PyHSM
pip install .
Development Install
pip install ".[dev]" # includes pytest + pytest-cov
pip install -e . # editable mode
Requirements
| Dependency | Version |
|---|---|
| Python | ≥ 3.11 |
| cryptography | ≥ 43.0.0, < 48.0.0 |
| argon2-cffi | ≥ 23.1.0, < 25.0.0 |
Quickstart
Get up and running in 3 steps:
1. Install
pip install vectorguard-pyhsm
2. Generate a Key and Encrypt
from hsm import PyHSM
with PyHSM("my-keystore.enc", master_password="strong-password-here") as hsm:
# Generate an AES-256 encryption key
hsm.generate_key("my-key", key_type="aes-256")
# Encrypt sensitive data
ciphertext = hsm.encrypt("my-key", "secret data")
print(f"Ciphertext: {ciphertext}")
# Decrypt it back
plaintext = hsm.decrypt("my-key", ciphertext)
print(f"Plaintext: {plaintext.decode()}")
3. Or Use the CLI
# Generate an AES-256 key
vectorguard-pyhsm --store my-keystore.enc generate my-key --type aes-256
# Encrypt data
vectorguard-pyhsm --store my-keystore.enc encrypt my-key -d "secret data"
# Decrypt data
vectorguard-pyhsm --store my-keystore.enc decrypt my-key -d "<ciphertext-hex>"
Python API — PyHSM Class
The PyHSM class is the primary interface. It manages the keystore, enforces policies, handles auditing, and exposes all cryptographic operations.
Constructor
from hsm import PyHSM
hsm = PyHSM(
storage_path="keystore.enc", # Path to encrypted keystore file
master_password="your-password", # Required, minimum 12 characters
audit_log_path="audit.jsonl", # Optional audit log path
session_timeout_s=300.0, # Session idle timeout (seconds)
rate_limit_max_ops=100, # Max operations per key per window
rate_limit_window_s=60.0, # Rate limit window (seconds)
)
Supports context manager usage for automatic session cleanup:
with PyHSM("keystore.enc", master_password="...") as hsm:
# operations here
pass
# Session closed, memory zeroized
Import Paths
# Primary class
from hsm import PyHSM
# Storage and errors
from hsm import KeyStore, TamperError
from hsm.backends import StorageBackend, FileBackend, MemoryBackend
# Security utilities
from hsm import SecureBytes, zeroize_bytearray
from hsm import AuditLog, RateLimiter, MetricsCollector
from hsm import run_self_tests
# Shamir secret sharing
from hsm import split_secret, reconstruct_secret, zeroize
# JWK support
from hsm import export_symmetric_jwk, export_ec_jwk
from hsm import export_rsa_jwk, export_ed25519_jwk
Key Generation
Method Signature
hsm.generate_key(
key_id: str, # Unique identifier for the key
key_type: str = "aes-256", # Key type (see Key Types table)
metadata: dict = None, # Optional metadata dict
policy: dict = None, # Optional policy dict
caller_id: str = None, # Caller identity for ACL/audit
) -> str # Returns key_id
Examples
# AES-256 for symmetric encryption
hsm.generate_key("encryption-key", key_type="aes-256")
# Ed25519 for fast signing (Solana, SSH)
hsm.generate_key("signing-key", key_type="ed25519")
# secp256k1 for Ethereum/Bitcoin transaction signing
hsm.generate_key("eth-wallet", key_type="ec-secp256k1")
# RSA-4096 for legacy systems
hsm.generate_key("legacy-rsa", key_type="rsa-4096")
# With policy constraints
hsm.generate_key("limited-key", key_type="aes-256", policy={
"max_operations": 10000,
"allowed_callers": ["payment-service", "auth-service"],
"allow_encrypt": True,
"allow_decrypt": True,
"expires_at": "2027-01-01T00:00:00Z",
"rotate_every_days": 90,
})
Other Key Lifecycle Methods
| Method | Description |
|---|---|
list_keys() | List all stored keys with metadata |
has_key(key_id) | Check if a key exists |
search_keys(key_type=, metadata=, status=, policy_filter=) | Search keys by criteria |
destroy_key(key_id) | Permanently destroy a key (zeroizes material) |
enforce_expiry() | Archive all expired keys |
Encryption & Decryption
AES keys support authenticated encryption using AES-256-GCM with hybrid nonces (random + counter + random) to eliminate birthday-bound collisions.
Method Signatures
hsm.encrypt(
key_id: str, # Key to encrypt with
plaintext: str | bytes, # Data to encrypt
caller_id: str = None, # Caller identity
) -> str # Hex-encoded ciphertext
hsm.decrypt(
key_id: str, # Key to decrypt with
ciphertext_hex: str, # Hex ciphertext from encrypt()
caller_id: str = None, # Caller identity
) -> bytes # Decrypted plaintext
Example
with PyHSM("keystore.enc", master_password="...") as hsm:
hsm.generate_key("data-key", key_type="aes-256")
# Encrypt a private key, API token, or any sensitive data
ciphertext = hsm.encrypt("data-key", "sk_live_abc123xyz")
# Later, decrypt it
plaintext = hsm.decrypt("data-key", ciphertext)
# plaintext = b"sk_live_abc123xyz"
decrypt() automatically selects the correct key version for each ciphertext.
Signing & Verification
Asymmetric keys (EC, RSA, Ed25519) support digital signatures. The private key never leaves the HSM; you get back only the signature.
Method Signatures
hsm.sign(
key_id: str, # Asymmetric key to sign with
message: str | bytes, # Data to sign
caller_id: str = None,
) -> str # Hex-encoded signature
hsm.verify(
key_id: str, # Key to verify with
message: str | bytes, # Original message
signature_hex: str, # Hex signature from sign()
caller_id: str = None,
) -> bool # True if valid
hsm.get_public_key(key_id: str) -> str # PEM-encoded public key
Blockchain Transaction Signing
with PyHSM("keystore.enc", master_password="...") as hsm:
# Generate or import a secp256k1 wallet key
hsm.generate_key("eth-wallet", key_type="ec-secp256k1")
# Sign a transaction hash (32 bytes)
tx_hash = bytes.fromhex("a1b2c3...") # Your tx hash
signature_hex = hsm.sign("eth-wallet", tx_hash)
# Parse DER signature into (r, s, v) for broadcasting
# The private key was unwrapped, used, and zeroized internally
Supported Signing Algorithms
| Key Type | Algorithm | Hash | Use Case |
|---|---|---|---|
| ec-p256 | ECDSA | SHA-256 | General-purpose, TLS |
| ec-p384 | ECDSA | SHA-384 | Higher security |
| ec-p521 | ECDSA | SHA-512 | Maximum NIST security |
| ec-secp256k1 | ECDSA | SHA-256 | Ethereum, Bitcoin |
| ed25519 | EdDSA | Built-in | Solana, SSH, high-perf |
| rsa-2048 | RSA-PSS | SHA-256 | Legacy compatibility |
| rsa-4096 | RSA-PSS | SHA-256 | Legacy, high-security |
Key Rotation
AES keys support rotation with automatic version tracking. Old ciphertexts remain decryptable because the version number is embedded in the ciphertext format.
Manual Rotation
with PyHSM("keystore.enc", master_password="...") as hsm:
# Rotate to a new version
new_version = hsm.rotate_key("data-key")
# new_version = 2
# New encryptions use version 2
ct_new = hsm.encrypt("data-key", "new data")
# Old ciphertexts still decrypt (version 1 auto-selected)
plaintext = hsm.decrypt("data-key", old_ciphertext)
Automatic Rotation
hsm.generate_key("auto-rotate-key", key_type="aes-256", policy={
"rotate_every_days": 90, # Auto-rotate every 90 days
})
encrypt() call, PyHSM checks if the current key version's age exceeds the policy. If so, it rotates before encrypting. This is lazy (on-demand) rotation.
JWK Import & Export
Import existing keys from standard JWK format (RFC 7517) or export keys for interoperability.
Import
# Import an existing EC key from JWK
jwk_data = {
"kty": "EC",
"crv": "secp256k1",
"x": "...",
"y": "...",
"d": "..." # private key component
}
hsm.import_key_jwk("imported-wallet", jwk_data)
Export
# Export requires allow_export policy
hsm.generate_key("exportable-key", key_type="ed25519", policy={
"allow_export": True,
})
jwk = hsm.export_jwk("exportable-key")
# Returns standard JWK dict
allow_export: True in their policy cannot be exported.
CLI Reference
The vectorguard-pyhsm CLI provides full access to all PyHSM operations from the terminal.
Global Options
| Flag | Default | Description |
|---|---|---|
--store PATH | keystore.enc | Path to the encrypted keystore file |
The master password is prompted interactively via getpass, or can be provided via the PYHSM_MASTER_PASSWORD environment variable for scripting.
All Commands
| Command | Description |
|---|---|
generate <key_id> | Generate a new cryptographic key |
list | List all keys in the keystore |
stores [dir] | List keystore files in a directory |
rotate <key_id> | Rotate an AES key to a new version |
delete <key_id> | Permanently destroy a key |
encrypt <key_id> | Encrypt data with an AES key |
decrypt <key_id> | Decrypt hex ciphertext |
sign <key_id> | Sign data with an asymmetric key |
verify <key_id> <msg> <sig> | Verify a signature (exit 0=valid, 1=invalid) |
pubkey <key_id> | Export the public key in PEM format |
split | Split a secret into Shamir shares |
reconstruct | Reconstruct a secret from Shamir shares |
metrics | Show operational metrics |
audit | Inspect or verify the audit log |
CLI: generate
Generate a new cryptographic key inside the keystore.
vectorguard-pyhsm --store <path> generate <key_id> [options]
Options
| Flag | Description |
|---|---|
--type TYPE | Key type: aes-128, aes-256, rsa-2048, rsa-4096, ec-p256, ec-p384, ec-p521, ec-secp256k1, ed25519 |
--no-encrypt | Disallow encryption with this key |
--no-decrypt | Disallow decryption with this key |
--max-operations N | Maximum total operations allowed |
--expires-at ISO8601 | Key expiration timestamp |
Examples
# Generate an AES-256 key
vectorguard-pyhsm generate vault-key --type aes-256
# Generate an Ed25519 signing key with max 5000 operations
vectorguard-pyhsm generate signer --type ed25519 --max-operations 5000
# Generate a key that expires
vectorguard-pyhsm generate temp-key --type aes-256 --expires-at 2027-06-01T00:00:00Z
CLI: encrypt / decrypt
# Encrypt with -d flag
vectorguard-pyhsm encrypt my-key -d "sensitive data"
# Encrypt from stdin
echo "sensitive data" | vectorguard-pyhsm encrypt my-key
# Decrypt hex ciphertext
vectorguard-pyhsm decrypt my-key -d "0200000001f383ad67..."
# Decrypt from stdin
echo "0200000001f383ad67..." | vectorguard-pyhsm decrypt my-key
CLI: sign / verify
vectorguard-pyhsm sign my-signing-key -d "message to sign"
# Output: hex-encoded signature
vectorguard-pyhsm verify my-signing-key "message to sign" "3045022100..."
# Exit code: 0 = VALID, 1 = INVALID
vectorguard-pyhsm pubkey my-signing-key
# Output: PEM-encoded public key
CLI: split / reconstruct
Split a secret into Shamir shares or reconstruct from shares.
vectorguard-pyhsm split -k 3 -n 5 -s "64656d6f2d6d61737465722d70617373776f7264"
# Output:
# Share 1: {"index": 1, "data": "d18a992fab..."}
# Share 2: {"index": 2, "data": "0985e4adb3..."}
# Share 3: {"index": 3, "data": "bc6a10ed35..."}
# Share 4: {"index": 4, "data": "ce2a97e318..."}
# Share 5: {"index": 5, "data": "7bc563a39e..."}
vectorguard-pyhsm reconstruct \
--share '{"index": 1, "data": "d18a992fab..."}' \
--share '{"index": 3, "data": "bc6a10ed35..."}' \
--share '{"index": 5, "data": "7bc563a39e..."}'
# Output: 64656d6f2d6d61737465722d70617373776f7264
Split Options
| Flag | Description |
|---|---|
-k, --threshold | Minimum shares needed to reconstruct (2–255) |
-n, --shares | Total number of shares to generate (k–255) |
-s, --secret | Hex-encoded secret to split |
CLI: audit / metrics
# Show all audit entries
vectorguard-pyhsm audit
# Verify audit log integrity (HMAC chain)
vectorguard-pyhsm audit --verify
# Filter by operation type
vectorguard-pyhsm audit --operation encrypt --since 2026-01-01T00:00:00Z
# Raw JSON output
vectorguard-pyhsm audit --raw
# Human-readable metrics
vectorguard-pyhsm metrics
# Prometheus text format
vectorguard-pyhsm metrics --prometheus
Audit Options
| Flag | Description |
|---|---|
--verify | Verify HMAC chain integrity |
--raw | Output raw JSON Lines |
--operation TYPE | Filter by operation type |
--key-id ID | Filter by key ID |
--since ISO8601 | Filter entries after this timestamp |
--until ISO8601 | Filter entries before this timestamp |
Key Types
PyHSM supports 9 key types covering symmetric encryption, ECDSA signing, EdDSA signing, and RSA signing.
| Key Type | CLI Name | Operations | Use Case |
|---|---|---|---|
| AES-128 | aes-128 | encrypt, decrypt | Lightweight symmetric encryption |
| AES-256 | aes-256 | encrypt, decrypt | Standard symmetric encryption (default) |
| RSA-2048 | rsa-2048 | sign, verify | Legacy RSA-PSS signatures |
| RSA-4096 | rsa-4096 | sign, verify | High-security RSA-PSS signatures |
| EC P-256 | ec-p256 | sign, verify | NIST standard, TLS, general-purpose |
| EC P-384 | ec-p384 | sign, verify | Higher NIST security level |
| EC P-521 | ec-p521 | sign, verify | Maximum NIST security level |
| EC secp256k1 | ec-secp256k1 | sign, verify | Ethereum, Bitcoin, blockchain |
| Ed25519 | ed25519 | sign, verify | Solana, Cosmos, SSH, high-performance |
Access Control
PyHSM provides three layers of access control that are enforced on every operation:
1. Caller ACLs
Restrict which services can use a key:
hsm.generate_key("payment-key", key_type="aes-256", policy={
"allowed_callers": ["payment-service", "billing-service"],
})
# This succeeds:
hsm.encrypt("payment-key", data, caller_id="payment-service")
# This raises ValueError + logs accessDenied:
hsm.encrypt("payment-key", data, caller_id="unknown-service")
2. Rate Limiting
Sliding-window rate limiting per key prevents abuse:
# Configured at HSM initialization
hsm = PyHSM(
"keystore.enc",
master_password="...",
rate_limit_max_ops=100, # 100 operations...
rate_limit_window_s=60.0, # ...per 60-second window
)
3. Operation Caps
Hard limit on total operations per key (lifetime):
hsm.generate_key("limited-key", key_type="aes-256", policy={
"max_operations": 10000, # After 10,000 ops, key is unusable
})
Policy Enforcement Order
- Caller ACL check (no side effects)
- Operation permission (
allow_encrypt,allow_decrypt,allow_sign) - Max operations check
- Expiry check
- Rate limiter (consumes a token only after all other checks pass)
Audit Log
Every operation is recorded in an HMAC-chained, tamper-evident audit log. Each entry's HMAC is computed over the entry content plus the previous entry's HMAC, forming a cryptographic chain similar to a blockchain.
Configuration
from hsm import AuditLog
audit = AuditLog(
log_path="pyhsm-audit.jsonl",
hmac_key=derived_key, # Derived from master via HKDF
webhook_url="https://siem.example.com/ingest", # Optional
max_bytes=50 * 1024 * 1024, # 50 MB rotation threshold
max_rotated_files=10, # Keep 10 rotated files
)
Tracked Operations
| Operation | Description |
|---|---|
encrypt | Data encrypted with a key |
decrypt | Data decrypted with a key |
sign | Message signed with a key |
verify | Signature verified |
generateKey | New key generated |
importKey | Key imported from JWK |
destroyKey | Key permanently destroyed |
rotateKey | Key rotated to new version |
exportKey | Key exported as JWK |
exportKeyDenied | Export attempt denied by policy |
accessDenied | Caller ACL violation |
rateLimited | Rate limit exceeded |
tamperDetected | Keystore integrity violation detected |
selfTestPass / selfTestFail | Startup self-test results |
Verification
# Programmatic verification
result = audit.verify()
# Returns -1 if chain is clean
# Returns sequence number of first corrupted entry otherwise
# CLI verification
vectorguard-pyhsm audit --verify
Webhook Shipping
When configured, each audit entry is POSTed to the webhook URL with an X-PyHSM-Signature HMAC header for authenticity verification.
Shamir Secret Sharing
Split the master password into N shares where any K shares can reconstruct it, but K-1 or fewer reveal zero information. Implemented over GF(256) using the AES irreducible polynomial.
Python API
from hsm import split_secret, reconstruct_secret, zeroize
# Split a secret into 5 shares, threshold of 3
secret = b"my-master-password"
shares = split_secret(secret, k=3, n=5)
# shares = [{"index": 1, "data": "hex..."}, ...]
# Reconstruct from any 3 shares
recovered = reconstruct_secret(shares[:3])
# recovered = bytearray(b"my-master-password")
# Always zeroize after use
zeroize(recovered)
Constraints
- 2 ≤ k ≤ n ≤ 255
- Secret must be non-empty
- Shares from different split ceremonies are not interchangeable
- K-1 shares reveal mathematically zero information (information-theoretic security)
Production Use
In production, distribute shares to separate custodians in different locations. At startup, gather the threshold number of shares to reconstruct the master password and unlock the HSM.
splitMasterPassword() and reconstructMasterPassword() helpers, plus support for injecting shares via the PYHSM_SHARES environment variable as comma-separated JSON.
Process Isolation
The highest security mode separates key material into a dedicated OS process communicating over a Unix domain socket. Even a full RCE in your application cannot read the HSM process's memory without a kernel exploit.
Architecture
┌─────────────────────┐ ┌─────────────────────┐
│ Your Application │ IPC │ PyHSM Process │
│ │ ──────► │ │
│ PyHSMClient │ Unix │ Keys in memory │
│ (sends requests) │ Socket │ Crypto operations │
│ │ ◄────── │ Zeroization │
└─────────────────────┘ └─────────────────────┘
TypeScript Client
import { PyHSMClient } from "pyhsm-ts/client";
const client = new PyHSMClient("/tmp/pyhsm.sock", "my-service");
// All operations go through IPC
const ciphertext = await client.encrypt("data-key", "secret");
const plaintext = await client.decrypt("data-key", ciphertext);
const health = await client.health();
// { status: "ok", uptime: 3600 }
Security Properties
- Socket permissions restricted to
0600 - Max IPC message size: 1 MB (prevents memory exhaustion)
- Caller authentication via HMAC over request payload
- Graceful shutdown on SIGTERM/SIGINT
- Your application never sees raw key bytes
Configuration
| Variable | Default | Description |
|---|---|---|
PYHSM_SOCKET_PATH | /tmp/pyhsm.sock | Unix socket path for IPC |
PYHSM_CALLER_SECRET | none | Shared HMAC secret for caller auth |
Environment Variables
Python
| Variable | Default | Description |
|---|---|---|
PYHSM_MASTER_PASSWORD | none | Master password for CLI scripting (avoid in production) |
PYHSM_LOG_LEVEL | WARNING | Logging level: DEBUG, INFO, WARNING, ERROR, CRITICAL |
PYHSM_ALLOW_PBKDF2_FALLBACK | 0 | Set to 1 to allow PBKDF2 fallback (testing only) |
PYHSM_AUDIT_HMAC_KEY | derived | Hex 32-byte audit HMAC key override |
PYHSM_AUDIT_WEBHOOK | none | URL for audit event webhook delivery |
TypeScript
| Variable | Default | Description |
|---|---|---|
PYHSM_MASTER_PASSWORD | none | Required (or use PYHSM_SHARES) |
PYHSM_SHARES | none | Comma-separated Shamir share JSONs for unlock |
PYHSM_KEYSTORE_PATH | ./pyhsm-keystore.enc | Keystore file location |
PYHSM_AUDIT_LOG_PATH | <store>.audit.jsonl | Audit log file path |
PYHSM_BACKUP_DIR | none | Backup directory |
PYHSM_SOCKET_PATH | /tmp/pyhsm.sock | IPC socket path (process isolation) |
PYHSM_CALLER_SECRET | none | Shared secret for IPC caller auth |
PYHSM_SESSION_TIMEOUT_MS | 300000 | Session idle timeout (milliseconds) |
PYHSM_RATE_LIMIT | 100 | Max operations per key per window |
PYHSM_RATE_WINDOW_MS | 60000 | Rate limit window (milliseconds) |
PYHSM_KEY_ID | pyhsm-master | Default key ID for singleton helpers |
Key Policies
Policies are set at key generation time and enforce security constraints throughout the key's lifecycle.
| Policy Field | Type | Description |
|---|---|---|
allow_encrypt | bool | Allow encryption operations (default: true) |
allow_decrypt | bool | Allow decryption operations (default: true) |
allow_sign | bool | Allow signing operations (default: true) |
allow_export | bool | Allow JWK export (default: false) |
max_operations | int | Maximum total operations (lifetime cap) |
expires_at | ISO 8601 | Key expiration timestamp |
rotate_every_days | int | Auto-rotation interval in days (AES only) |
allowed_callers | list[str] | Authorized caller IDs (ACL) |
Example: Full Policy
hsm.generate_key("production-key", key_type="aes-256", policy={
"allow_encrypt": True,
"allow_decrypt": True,
"allow_export": False,
"max_operations": 1000000,
"expires_at": "2028-01-01T00:00:00Z",
"rotate_every_days": 90,
"allowed_callers": ["api-gateway", "worker-service"],
})
TypeScript Layer
PyHSM includes a full TypeScript/Node.js implementation with feature parity. It uses AES-256-GCM-SIV (nonce-misuse resistant) via @noble/ciphers.
Installation
cd pyhsm-ts
npm install
npm run build
Usage
import { PyHSM } from "./core";
// Async factory (uses Argon2id)
const hsm = await PyHSM.create({
keystorePath: "./keystore.enc",
masterPassword: "strong-password",
auditLogPath: "./audit.jsonl",
});
// Generate a key
await hsm.generateKey("my-key", { type: "aes-256" });
// Encrypt and decrypt
const ct = await hsm.encrypt("my-key", "secret data");
const pt = await hsm.decrypt("my-key", ct);
// Clean up
hsm.close();
Key Differences from Python
- Uses AES-256-GCM-SIV (nonce-misuse resistant) instead of AES-256-GCM
- Async factory pattern:
PyHSM.create()for Argon2id, sync constructor for PBKDF2 fallback - Deferred persistence with dirty-flag flushing
SecureBufferclass for deterministic zeroization- Pinned exact dependency versions in
package.json