PyHSM Documentation

Current version v2.3.2

Complete reference for the PyHSM software Key Management Service. Generate keys, encrypt data, sign messages, manage lifecycles, and audit every operation.

sudo apt install pipx -y && pipx install vectorguard-pyhsm

⚠️ Migrating from v1.x to v2.0.0

v2.0.0 is a major release with breaking changes. Existing v1.x keystores are readable — PyHSM migrates the KDF automatically on the next write. The code changes required are minimal but mandatory.

Breaking Changes

Changev1.xv2.0.0
Constructor arguments PyHSM("path", "pw") PyHSM(storage_path="path", master_password="pw")
decrypt() return type str or bytes Always bytes — add .decode("utf-8") if needed
get_audit_log() return type Raw list AuditLog object — call .export_jsonl() for the list
allow_export policy default True (when unset, some paths) Always False — must be set explicitly
CLI generate --type Optional (defaulted to aes-256) Required — no implicit default
TypeScript factory Sync constructor supported Use PyHSM.create() for production; sync constructor emits deprecation warning

Migration Steps

  1. Upgrade: pipx upgrade vectorguard-pyhsm
  2. Open your keystore once — PyHSM automatically migrates the KDF from PBKDF2 to Argon2id on the next write.
  3. Update constructor calls to use keyword arguments.
  4. Add .decode("utf-8") anywhere you pass decrypt() output directly to a str-expecting API.
  5. Replace hsm.get_audit_log() list access with hsm.get_audit_log().export_jsonl().
  6. Add "allow_export": True to any key policy that relies on JWK export.

⚠️ v2.2.0 IPC Server: Required Authentication

v2.2.0 makes --ipc-secret mandatory for the IPC server. The server refuses to start without it. This is the only breaking change from v2.1.0.

Generating and Using the IPC Secret

Generate a 256-bit shared secret (once, store securely)
openssl rand -hex 32 > /run/secrets/pyhsm-ipc-secret
chmod 600 /run/secrets/pyhsm-ipc-secret
chown pyhsm:pyhsm /run/secrets/pyhsm-ipc-secret
Start the server
vectorguard-pyhsm-server \
    --store /var/lib/pyhsm/keystore.enc \
    --password-file /run/secrets/pyhsm-password \
    --socket /run/pyhsm/pyhsm.sock \
    --ipc-secret "$(cat /run/secrets/pyhsm-ipc-secret)"
Connect from your application (Python)
from hsm.ipc_client import IPCClient

ipc_secret = open("/run/secrets/pyhsm-ipc-secret").read().strip()
with IPCClient("/run/pyhsm/pyhsm.sock", ipc_secret=ipc_secret) as hsm:
    ct = hsm.encrypt("my-key", "sensitive data")

Other Security Improvements in v2.2.0

ChangeImpact
IPC global lock replaced with per-key locking + connection timeoutEliminates DoS from slow operations; read-only ops hold no lock
Shamir: per-share HMAC-SHA256 (256-bit, index-bound)Replaces 4-byte truncated checksum; prevents share transplantation
Startup KATs expanded from 4 to 10 primitivesAll active crypto verified at startup against NIST/RFC vectors
Audit log cross-rotation chain (rotationSeal/rotationOpen)Deletion of any rotated log file is cryptographically detectable

⚠️ v2.3.0 License Change: MIT → AGPL-3.0

v2.3.0 contains a single change: the license has been updated from MIT to the GNU Affero General Public License v3.0 (AGPL-3.0). There are no code changes in this release — the version bump exists solely to mark the license transition clearly in the package history.

What This Means

Use CaseBefore v2.3.0v2.3.0+ (AGPL-3.0)
Open-source projects Free to use Free to use
Personal / research use Free to use Free to use
Proprietary product distribution Free to use Requires commercial license
Network service (SaaS) without releasing source Free to use Requires commercial license

Users who had v2.2.5 already installed in their infrastructure prior to October 28, 2026 are unaffected by this change.

Commercial licensing: If you are embedding PyHSM in a proprietary product or running it as part of a network service without releasing your application source under AGPL, contact pavon@vectorguardlabs.com or see COMMERCIAL_LICENSE.md in the repository.

⚠️ v2.3.1 Security Hardening (No Breaking Changes)

v2.3.1 (released October 29, 2026) ships four targeted security fixes and 10 new regression tests. There are no breaking changes from v2.3.0. Update with pipx upgrade vectorguard-pyhsm.

What Changed

FixImpact
Thread-safe rate limiter (RateLimiter.allow(), usage(), reset()) Prevents two concurrent threads from both passing the rate-limit check before either records the operation; eliminates the 2× burst over-admission race.
Constant-time HMAC comparisons in audit.verify(), audit.verify_across_rotations(), and the legacy Shamir checksum path Replaces plain != with hmac.compare_digest() everywhere. Eliminates timing-oracle leakage on HMAC-derived strings.
RSA key import size enforcement in import_key_jwk() Rejects RSA keys < 2048 bits with a clear error citing NIST SP 800-131A Rev. 2. Previously, 512 and 1024-bit keys were silently accepted.
Stronger RSA-PSS and Ed25519 startup KATs RSA-PSS KAT now uses a fixed embedded private key (fingerprint-pinned) to catch library regressions. Both RSA-PSS and Ed25519 KATs add wrong-key rejection checks.

Updated Test Counts

v2.3.1 adds 10 new regression tests: TestRateLimiterThreadSafety, TestJWKWeakKeyRejection, TestAuditVerifyConstantTime, and TestKATVectors. Total test count is now 274 tests (180 Python + 94 TypeScript).

RSA Import Validation Example

Importing a weak RSA key now raises an error
from hsm import PyHSM

with PyHSM(storage_path="keystore.enc", master_password="...") as hsm:
    # This will raise ValueError in v2.3.1+
    hsm.import_key_jwk("weak-rsa", {
        "kty": "RSA",
        "n": "...",  # 1024-bit modulus
        "e": "AQAB",
        "d": "...",
    })
    # ValueError: RSA key size 1024 bits is below the minimum of 2048 bits
    # (NIST SP 800-131A Rev. 2). Use rsa-2048 or rsa-4096.

Overview

PyHSM is a production-ready software Key Management Service (KMS) that provides:

  • AES-256-GCM (Python) / AES-256-GCM-SIV (TypeScript) authenticated encryption with Argon2id key derivation (required, OWASP recommended)
  • 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)
  • Salt-bound KEK derivation — KEK uses a dedicated salt via full Argon2id → HKDF path
  • HMAC-chained tamper-evident audit logs with configurable rotation and cross-rotation chain integrity (rotationSeal/rotationOpen + verify_across_rotations())
  • Shamir M-of-N secret sharing over GF(256) with per-share HMAC-SHA256 integrity (v2.2.0+)
  • Key rotation with version tracking; automatic rotation via rotate_every_days policy
  • Key metadata search (by type, metadata tags, status, or policy fields)
  • Caller ACLs, rate limiting, and operation caps
  • Encrypted backup/restore with HMAC verification
  • Prometheus metrics + OpenTelemetry (OTLP) JSON export
  • Process isolation mode (Unix domain socket IPC) with mandatory HMAC mutual authentication and per-key locking (v2.2.0+)
  • Pluggable storage backends (file, memory, or custom)
  • Dual implementation: Python and TypeScript with full feature parity
  • Self-test Known-Answer Tests (KATs) on startup — all 10 active primitives verified against published NIST/RFC vectors
  • 274 tests (180 Python + 94 TypeScript), 80%+ coverage enforced in CI

PyHSM is open-source software licensed under the AGPL-3.0. It requires no hardware, no cloud dependency, and no per-operation fees. Commercial use in proprietary products or network services without releasing application source requires a commercial license.

Installation

From PyPI (Recommended)

pipx install vectorguard-pyhsm

From Source

git clone https://github.com/pavondunbar/PyHSM.git
cd PyHSM
pipx install .

Development Install

pipx install ".[dev]"    # includes pytest + pytest-cov
pipx install -e .        # editable mode

Requirements

DependencyVersion
Python≥ 3.11
cryptography≥ 43.0.0, < 44.0.0
argon2-cffi≥ 23.1.0, < 25.0.0

Quickstart

Get up and running in 3 steps:

1. Install

pipx install vectorguard-pyhsm

2. Generate a Key and Encrypt

Python
from hsm import PyHSM

# v2: all constructor parameters must be passed as keyword arguments
with PyHSM(storage_path="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 — decrypt() always returns bytes in v2
    plaintext = hsm.decrypt("my-key", ciphertext)
    print(f"Plaintext: {plaintext.decode('utf-8')}")

3. Or Use the CLI

Terminal
# 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

# v2: all parameters MUST be keyword arguments (positional args removed)
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)
    license_file="/etc/pyhsm/license.jwt",  # Commercial license token file (v2.3.0+, optional)
    # license_token="",     # Or: inline JWT string
)
v2 breaking change: Positional arguments are no longer accepted. All parameters must be passed by name. PyHSM("keystore.enc", "password") will raise a TypeError — use PyHSM(storage_path="keystore.enc", master_password="password") instead.

Supports context manager usage for automatic session cleanup:

with PyHSM(storage_path="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

Generate keys of different types
# 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

MethodDescription
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 (always bytes in v2)
v2 breaking change: decrypt() now always returns bytes. If you previously relied on getting a str back, add an explicit .decode("utf-8") call: plaintext.decode("utf-8").

Example

with PyHSM(storage_path="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 — returns bytes in v2
    plaintext = hsm.decrypt("data-key", ciphertext)
    # plaintext = b"sk_live_abc123xyz"
    secret_str = plaintext.decode("utf-8")  # explicit decode if you need str
Version routing: The ciphertext includes a version prefix. After key rotation, 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

Ethereum transaction signing with secp256k1
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 TypeAlgorithmHashUse Case
ec-p256ECDSASHA-256General-purpose, TLS
ec-p384ECDSASHA-384Higher security
ec-p521ECDSASHA-512Maximum NIST security
ec-secp256k1ECDSASHA-256Ethereum, Bitcoin
ed25519EdDSABuilt-inSolana, SSH, high-perf
rsa-2048RSA-PSSSHA-256Legacy compatibility
rsa-4096RSA-PSSSHA-256Legacy, 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(storage_path="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

Set rotation policy at key creation
hsm.generate_key("auto-rotate-key", key_type="aes-256", policy={
    "rotate_every_days": 90,  # Auto-rotate every 90 days
})
How auto-rotation works: On each 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. Supported key types: AES (oct), EC P-256/P-384/P-521/secp256k1, Ed25519 (OKP), and RSA.

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)

# Import an RSA key from JWK (v2 addition)
# NOTE (v2.3.1+): RSA keys below 2048 bits are rejected per NIST SP 800-131A Rev. 2
hsm.import_key_jwk("rsa-key", {
    "kty": "RSA",
    "n": "...",   # base64url modulus — must be 2048 or 4096 bits
    "e": "AQAB", # public exponent
    "d": "...",  # private exponent
})

Export

# v2: allow_export defaults to False — must be explicitly set to True
hsm.generate_key("exportable-key", key_type="ed25519", policy={
    "allow_export": True,   # required in v2; previously could be unset
})

jwk = hsm.export_jwk("exportable-key")
# Returns standard JWK dict
v2 breaking change: allow_export now defaults to False on all keys. In v1.x, some code paths allowed export when the policy key was absent. If you rely on JWK export, add "allow_export": True explicitly to the key's policy.

CLI Reference

The vectorguard-pyhsm CLI provides full access to all PyHSM operations from the terminal.

Global Options

FlagDefaultDescription
--store PATHkeystore.encPath 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

CommandDescription
generate <key_id>Generate a new cryptographic key
listList all keys in the keystore
stores [dir]List keystore files in a directory (no password required)
rotate <key_id>Rotate an AES key to a new version
delete <key_id> [--yes]Permanently destroy a key; --yes / -y skips confirmation prompt
licenseShow active license tier, licensee, expiry, and key limits (commercial or AGPL)
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
splitSplit a secret into Shamir shares
reconstructReconstruct a secret from Shamir shares
metrics [--prometheus]Show operational metrics; --prometheus outputs Prometheus text format
auditInspect or verify the audit log

CLI: generate

Generate a new cryptographic key inside the keystore.

Usage
vectorguard-pyhsm --store <path> generate <key_id> [options]

Options

FlagDescription
--type TYPERequired. Key type: aes-128, aes-256, rsa-2048, rsa-4096, ec-p256, ec-p384, ec-p521, ec-secp256k1, ed25519
--no-encryptDisallow encryption with this key
--no-decryptDisallow decryption with this key
--max-operations NMaximum total operations allowed
--expires-at ISO8601Key expiration timestamp
v2 breaking change: --type is now required. The implicit aes-256 default has been removed to prevent accidental key type mismatches.

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
# Encrypt with -d flag
vectorguard-pyhsm encrypt my-key -d "sensitive data"

# Encrypt from stdin
echo "sensitive data" | vectorguard-pyhsm encrypt my-key
Decrypt
# Decrypt hex ciphertext
vectorguard-pyhsm decrypt my-key -d "0200000001f383ad67..."

# Decrypt from stdin
echo "0200000001f383ad67..." | vectorguard-pyhsm decrypt my-key

CLI: sign / verify

Sign a message
vectorguard-pyhsm sign my-signing-key -d "message to sign"
# Output: hex-encoded signature
Verify a signature
vectorguard-pyhsm verify my-signing-key "message to sign" "3045022100..."
# Exit code: 0 = VALID, 1 = INVALID
Export public key
vectorguard-pyhsm pubkey my-signing-key
# Output: PEM-encoded public key

CLI: split / reconstruct

Split a secret into Shamir shares or reconstruct from shares.

Split a secret into 5 shares (3 required to reconstruct)
vectorguard-pyhsm split -k 3 -n 5 -s "64656d6f2d6d61737465722d70617373776f7264"

# Output (v2.2.0+ format with per-share HMAC-SHA256):
# Share 1: {"index": 1, "data": "d18a992fab...", "share_hmac": "...", "hmac_salt": "...", "reconstruction_hmac": "..."}
# Share 2: {"index": 2, "data": "0985e4adb3...", "share_hmac": "...", "hmac_salt": "...", "reconstruction_hmac": "..."}
# Share 3: {"index": 3, "data": "bc6a10ed35...", "share_hmac": "...", "hmac_salt": "...", "reconstruction_hmac": "..."}
# Share 4: {"index": 4, "data": "ce2a97e318...", "share_hmac": "...", "hmac_salt": "...", "reconstruction_hmac": "..."}
# Share 5: {"index": 5, "data": "7bc563a39e...", "share_hmac": "...", "hmac_salt": "...", "reconstruction_hmac": "..."}
Reconstruct from any 3 shares
vectorguard-pyhsm reconstruct \
  --share '{"index": 1, "data": "d18a992fab...", "share_hmac": "...", "hmac_salt": "...", "reconstruction_hmac": "..."}' \
  --share '{"index": 3, "data": "bc6a10ed35...", "share_hmac": "...", "hmac_salt": "...", "reconstruction_hmac": "..."}' \
  --share '{"index": 5, "data": "7bc563a39e...", "share_hmac": "...", "hmac_salt": "...", "reconstruction_hmac": "..."}'
# Output: 64656d6f2d6d61737465722d70617373776f7264
v2.2.0+ share format: Shares now include share_hmac, hmac_salt, and reconstruction_hmac fields. Old shares with only index and data remain backward-compatible via the legacy checksum verification path.

Split Options

FlagDescription
-k, --thresholdMinimum shares needed to reconstruct (2–255)
-n, --sharesTotal number of shares to generate (k–255)
-s, --secretHex-encoded secret to split

CLI: audit / metrics

View audit log
# 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
View metrics
# Human-readable metrics
vectorguard-pyhsm metrics

# Prometheus text format
vectorguard-pyhsm metrics --prometheus

Audit Options

FlagDescription
--verifyVerify HMAC chain integrity
--rawOutput raw JSON Lines
--operation TYPEFilter by operation type
--key-id IDFilter by key ID
--since ISO8601Filter entries after this timestamp
--until ISO8601Filter entries before this timestamp

CLI: license

Show the active license tier, licensee, expiry, and per-tier limits. Available in v2.3.0+.

Check license with a commercial token file
vectorguard-pyhsm --license-file /etc/pyhsm/license.jwt license
Check license in AGPL mode (no token)
vectorguard-pyhsm --store keystore.enc license
Inject token via environment variable
export PYHSM_LICENSE_TOKEN="$(cat /etc/pyhsm/license.jwt)"
vectorguard-pyhsm --store keystore.enc license

Python API

info = hsm.get_license_info()
print(info.tier.value)      # "startup" | "growth" | "scale" | "agpl"
print(info.licensee)        # e.g. "you@example.com" or "agpl-user"
print(info.expires_at)      # ISO 8601 timestamp
print(info.max_keys)        # 5 for Startup, None for Growth/Scale/AGPL
print(info.is_commercial)   # True for paid tiers
Air-gap safe: License verification happens entirely offline using an embedded RSA-2048 public key. No network call is required. Invalid, expired, or tampered tokens raise a LicenseError at construction time before any key operation is permitted.

Key Types

PyHSM supports 9 key types covering symmetric encryption, ECDSA signing, EdDSA signing, and RSA signing.

Key TypeCLI NameOperationsUse Case
AES-128aes-128encrypt, decryptLightweight symmetric encryption
AES-256aes-256encrypt, decryptStandard symmetric encryption (default)
RSA-2048rsa-2048sign, verifyLegacy RSA-PSS signatures
RSA-4096rsa-4096sign, verifyHigh-security RSA-PSS signatures
EC P-256ec-p256sign, verifyNIST standard, TLS, general-purpose
EC P-384ec-p384sign, verifyHigher NIST security level
EC P-521ec-p521sign, verifyMaximum NIST security level
EC secp256k1ec-secp256k1sign, verifyEthereum, Bitcoin, blockchain
Ed25519ed25519sign, verifySolana, 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

  1. Caller ACL check (no side effects)
  2. Operation permission (allow_encrypt, allow_decrypt, allow_sign)
  3. Max operations check
  4. Expiry check
  5. 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

OperationDescription
encryptData encrypted with a key
decryptData decrypted with a key
signMessage signed with a key
verifySignature verified
generateKeyNew key generated
importKeyKey imported from JWK
destroyKeyKey permanently destroyed
rotateKeyKey rotated to new version
exportKeyKey exported as JWK
exportKeyDeniedExport attempt denied by policy
accessDeniedCaller ACL violation
rateLimitedRate limit exceeded
tamperDetectedKeystore integrity violation detected
selfTestPass / selfTestFailStartup self-test results (all 10 KAT primitives)
rotationSealAppended to closing log file at rotation; contains final chain HMAC
rotationOpenFirst entry in new log file; records the previous seal HMAC

Verification

# get_audit_log() returns an AuditLog object in v2 (not a raw list)
audit = hsm.get_audit_log()

# Programmatic verification (active log file only)
result = audit.verify()
# Returns -1 if chain is clean
# Returns sequence number of first corrupted entry otherwise

# Cross-rotation chain verification (v2.2.0+)
# Verifies the full chain across all retained rotated files.
# Deletion of any rotated file is detectable.
from hsm.audit import AuditLog
log = AuditLog('/var/log/pyhsm/keystore.enc.audit.jsonl', hmac_key=key)
result = log.verify_across_rotations()
# {"ok": True, "files_checked": 5}  — or error on tamper

# Export entries as a list of dicts (SIEM-ready)
entries = audit.export_jsonl()                         # all entries
entries = audit.export_jsonl(operation="encrypt")      # filter by operation

# CLI verification (active log)
vectorguard-pyhsm audit --verify

# CLI export for SIEM (CEF format for Splunk/QRadar/ArcSight)
vectorguard-pyhsm audit --siem-format cef | logger -t pyhsm -p security.info

# CLI export as JSON for Elastic/OpenSearch
vectorguard-pyhsm audit --siem-format json
v2 breaking change: get_audit_log() now returns an AuditLog object instead of a raw list. Call .export_jsonl() on the returned object to get the list of entries.

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...", "share_hmac": "...", "hmac_salt": "...", "reconstruction_hmac": "..."}, ...]

# Reconstruct from any 3 shares
recovered = reconstruct_secret(shares[:3])
# recovered = bytearray(b"my-master-password")

# Always zeroize after use
zeroize(recovered)
Share format (v2.2.0+): Each share object contains index, data (hex-encoded share bytes), share_hmac (HMAC-SHA256 of index_byte || data, keyed on hmac_salt), hmac_salt (32-byte random hex, shared across all shares from the same split), and reconstruction_hmac (HMAC-SHA256 of the full reconstructed secret, catches insufficient-share reconstruction). Old shares with only a checksum field remain backward-compatible.

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.

TypeScript: The TypeScript layer provides 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

Process isolation model
┌─────────────────────┐         ┌─────────────────────┐
│  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, owned by the pyhsm service account
  • Mutual authentication is mandatory — --ipc-secret required at server startup; unauthenticated requests rejected before any operation
  • Per-key locking — operations on different keys run in parallel (no global lock DoS)
  • 30-second per-connection read timeout — stalled clients dropped cleanly
  • Read-only operations (health, list_keys, has_key, get_public_key, get_metrics) hold no lock at all
  • Max IPC message size: 1 MB (prevents memory exhaustion)
  • Graceful shutdown on SIGTERM/SIGINT
  • Your application never sees raw key bytes

Configuration

VariableDefaultDescription
PYHSM_SOCKET_PATH/tmp/pyhsm.sockUnix socket path for IPC
PYHSM_IPC_SECRET / --ipc-secretRequiredShared HMAC secret for mutual authentication (mandatory since v2.2.0)
PYHSM_CALLER_SECRETnoneTypeScript layer: shared secret for IPC caller auth

Environment Variables

Python

VariableDefaultDescription
PYHSM_MASTER_PASSWORDnoneMaster password for CLI scripting (avoid in production)
PYHSM_LOG_LEVELWARNINGLogging level: DEBUG, INFO, WARNING, ERROR, CRITICAL
PYHSM_ALLOW_PBKDF2_FALLBACK0Set to 1 to allow PBKDF2 fallback (testing only)
PYHSM_AUDIT_HMAC_KEYderivedHex 32-byte audit HMAC key override
PYHSM_AUDIT_WEBHOOKnoneURL for audit event webhook delivery
PYHSM_LICENSE_TOKENnoneInline commercial JWT license token (v2.3.0+)
PYHSM_LICENSE_FILEnonePath to commercial JWT license token file (v2.3.0+)

TypeScript

VariableDefaultDescription
PYHSM_MASTER_PASSWORDnoneRequired (or use PYHSM_SHARES)
PYHSM_SHARESnoneComma-separated Shamir share JSONs for unlock
PYHSM_KEYSTORE_PATH./pyhsm-keystore.encKeystore file location
PYHSM_AUDIT_LOG_PATH<store>.audit.jsonlAudit log file path
PYHSM_BACKUP_DIRnoneBackup directory
PYHSM_SOCKET_PATH/tmp/pyhsm.sockIPC socket path (process isolation)
PYHSM_CALLER_SECRETnoneShared secret for IPC caller auth
PYHSM_SESSION_TIMEOUT_MS300000Session idle timeout (milliseconds)
PYHSM_RATE_LIMIT100Max operations per key per window
PYHSM_RATE_WINDOW_MS60000Rate limit window (milliseconds)
PYHSM_KEY_IDpyhsm-masterDefault key ID for singleton helpers

Key Policies

Policies are set at key generation time and enforce security constraints throughout the key's lifecycle.

Policy FieldTypeDescription
allow_encryptboolAllow encryption operations (default: true)
allow_decryptboolAllow decryption operations (default: true)
allow_signboolAllow signing operations (default: true)
allow_exportboolAllow JWK export (default: false — must be explicitly set to true)
max_operationsintMaximum total operations (lifetime cap)
expires_atISO 8601Key expiration timestamp
rotate_every_daysintAuto-rotation interval in days (AES only)
allowed_callerslist[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"],
})

Compliance Coverage

PyHSM is a software key management system. It uses FIPS-approved algorithms but is not FIPS 140-2/3 certified. For environments where certification of the specific binary is mandatory (PCI-DSS Level 1 SAQ D, FedRAMP High, DoD IL4+), a validated hardware HSM is required. PyHSM is appropriate as a software KMS for all other environments where software key management is acceptable.

Framework Coverage Summary

FrameworkCoverageNotes
SOC 2 Type IIPartialCC6.1, CC6.2, CC6.6, CC7.2, CC9.1 covered; CC6.3, CC7.3 partial; CC6.8 out of scope
PCI-DSS v4.0PartialReq 3.5–3.7, 6.3, 8.2, 10.2, 10.3, 10.5 covered; Req 12.3.3 not met (no FIPS cert)
HIPAA Security RulePartial§164.312(a)(1), §164.312(b), §164.308(a)(1)(ii)(D) covered; physical safeguards out of scope
NIST SP 800-57MeetsFull key lifecycle; all algorithms per SP 800-131A Rev. 2; 10-primitive KATs
NIST SP 800-63BMeetsArgon2id KDF, 12-char minimum, weak-password blocklist, Shamir with per-share HMAC
ISO/IEC 27001:2022PartialA.8.24, A.8.10, A.8.15, A.8.16, A.5.33, A.8.20 covered; A.8.7, A.8.9 partial
FIPS 140-2/3Does not meetFIPS-approved algorithms; not CMVP-validated; Argon2id is not a FIPS-approved KDF
GDPR Article 32Meets (technical)Encryption, integrity, availability, regular testing covered; organizational measures out of scope

Audit Evidence Package

When preparing for a compliance audit, provide the following:

  1. THREAT_MODEL.md — security boundary documentation
  2. SECURITY_POLICY.md — formal security policy
  3. COMPLIANCE.md — per-control coverage matrix
  4. Audit log export — audit --verify && audit --raw
  5. Cross-rotation chain verification — output of verify_across_rotations() showing ok: true
  6. Dependency scan results — pip-audit output from latest CI run
  7. Test coverage report — from latest CI run (76%+ line coverage)
  8. requirements.lock — hash-pinned dependency manifest
  9. KAT results — selfTestPass entries from the audit log confirming all 10 primitives passed
v2.2.5 compliance improvements: IPC mandatory mutual authentication strengthens CC6.1, Req 8.2, A.8.20, and §164.312(a)(1). Per-share HMAC-SHA256 on Shamir shares strengthens CC6.2, Req 3.6, and A.8.24. Cross-rotation audit chain integrity strengthens CC7.2, Req 10.3, §164.312(b), and A.5.33. The expanded 10-primitive KAT suite strengthens NIST SP 800-57 §8.1 evidence and FIPS 140-2 Level 1 self-test posture.

Deployment Hardening

For production deployments, follow the full guide in HARDENING.md in the repository. The critical requirements are summarized here.

Required Steps

1. Dedicated service account
useradd --system --no-create-home --shell /usr/sbin/nologin pyhsm
install -d -m 750 -o pyhsm -g pyhsm /var/lib/pyhsm /run/pyhsm /var/log/pyhsm
2. Password injection via file (preferred over env var)
# Place on tmpfs so it never touches disk
mount -t tmpfs -o size=1m,mode=700 tmpfs /run/secrets
printf '%s' 'YourStr0ng!Passphrase' > /run/secrets/pyhsm-password
chmod 600 /run/secrets/pyhsm-password
chown pyhsm:pyhsm /run/secrets/pyhsm-password
3. IPC secret (mandatory since v2.2.0)
openssl rand -hex 32 > /run/secrets/pyhsm-ipc-secret
chmod 600 /run/secrets/pyhsm-ipc-secret
chown pyhsm:pyhsm /run/secrets/pyhsm-ipc-secret
4. Reproducible dependency install
pipx install --require-hashes -r requirements.lock
5. Audit log cross-rotation verification
python3 -c "
from hsm.audit import AuditLog
import sys
key = open('/run/secrets/pyhsm-audit-key', 'rb').read()
log = AuditLog('/var/log/pyhsm/keystore.enc.audit.jsonl', hmac_key=key)
result = log.verify_across_rotations()
if result['ok']:
    print('Audit chain OK — files checked:', result['files_checked'])
else:
    print('TAMPER DETECTED:', result['first_error'], file=sys.stderr)
    sys.exit(1)
"

Pre-Production Checklist

[ ] PyHSM runs as a dedicated unprivileged user (not root)
[ ] Keystore file: chmod 600, owned by pyhsm user
[ ] Password file: chmod 600, on tmpfs
[ ] IPC secret file: chmod 600, on tmpfs (openssl rand -hex 32)
[ ] IPC server started with --ipc-secret (mandatory as of v2.2.0)
[ ] Audit log: chmod 640, shipped to SIEM
[ ] verify_across_rotations() returns ok=true
[ ] Swap encrypted or disabled
[ ] pipx install --require-hashes -r requirements.lock
[ ] systemd unit: NoNewPrivileges=true, MemoryDenyWriteExecute=true
[ ] Backup tested with verify-backup
[ ] NTP synchronized (key expiry depends on accurate time)
[ ] auditd watching keystore and audit log files

See HARDENING.md for complete systemd unit configuration, Docker hardening flags, swap/memory protection, and Shamir key ceremony procedures.

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

TypeScript — async factory (production, Argon2id)
import { PyHSM } from "./core";

// v2: PyHSM.create() is the ONLY supported factory for production use
// It guarantees Argon2id KDF (64 MB / 3 passes / 4 threads)
const hsm = await PyHSM.create({
  storePath: "./keystore.enc",
  masterPassword: "strong-password",
});

// Generate a key
hsm.generateKey("my-key");

// Encrypt and decrypt
const ct = hsm.encrypt("my-key", "secret data");
const pt = hsm.decrypt("my-key", ct);  // returns string

// Clean up
hsm.closeSession();
v2 breaking change: The synchronous constructor (new PyHSM({...})) is retained for testing only and now emits a deprecation warning at runtime. Use await PyHSM.create({...}) in all production code.

Key Differences from Python

  • Uses AES-256-GCM-SIV (nonce-misuse resistant) instead of AES-256-GCM
  • PyHSM.create() is the production factory (Argon2id); sync constructor is testing-only and emits a deprecation warning
  • Deferred persistence with dirty-flag flushing for higher throughput
  • SecureBuffer class for deterministic zeroization
  • Pinned exact dependency versions in package.json