A memory-safe software KMS in Rust: classical and post-quantum signing, BIP32/44 HD wallets, seed-derived symmetric encryption, multi-identity isolation, and a PKCS#11 provider.
Status: Experimental — not for production. softKMS is a personal project for exploring classical and modern cryptography (HD derivation, post-quantum FALCON, seed-derived symmetric keys) end-to-end. It is well-tested and security-conscious, but has not had an independent audit and intentionally omits some production features (see Roadmap). Use it for learning, development/testing, and research — at your own risk.
Good for: learning how a KMS works, HD-wallet and post-quantum experimentation, PKCS#11 exploration, and local development/testing of signing and symmetric-encryption workflows.
- Why softKMS?
- Key Features
- Quick Start
- Unlock Lifecycle
- Identity-Based Access Control
- Symmetric Encryption
- Architecture
- Installation
- Docker
- Configuration
- Quick Commands
- Project Structure
- Development
- Project Status
- Security
- Documentation
- Contributing
- License
| Feature | SoftHSM | softKMS |
|---|---|---|
| Language | C | Rust (memory-safe) |
| HD Wallets | ❌ | ✅ BIP32/44 (Ed25519) |
| Signing | RSA / ECC | Ed25519, P-256, Falcon-512/1024 (post-quantum) |
| Symmetric | — | ✅ Seed-derived AES-256-GCM (mnemonic-recoverable) |
| APIs | PKCS#11 only | PKCS#11 + gRPC + REST + CLI |
| Deployment | Manual | Docker (+ Debian/systemd packaging) |
| Identity | Single user | Multi-identity with namespace isolation |
- 🔐 Encrypted at rest — AES-256-GCM with a PBKDF2-derived master key.
- 🔒 Locked-boot model — the daemon starts locked and holds no secret; an operator unlocks it over a local admin channel. No passphrase in any file or environment variable.
- 👥 Identity isolation — multi-tenant, ECC-based identities (Ed25519 default, P-256 optional) with bearer-token auth; each identity sees only its own keys.
- 🌳 HD wallets — BIP32/BIP44 hierarchical deterministic derivation (Ed25519), plus deterministic P-256 (WebAuthn-style) and watch-only xpub import.
- 🧬 Seed-derived symmetric keys — AES-256-GCM keys derived from the BIP39 seed via HKDF-SHA512, so symmetric secrets are recoverable from the mnemonic (or generate random keys).
- 🛡️ Post-quantum — Falcon-512 / Falcon-1024 signatures (NIST PQC).
- 🔄 Key export — re-wrap keys for OpenSSH / GPG without exposing plaintext.
- 🔌 Multiple APIs — PKCS#11, gRPC, REST, CLI.
- 🔐 Optional TLS — REST can serve HTTPS (off by default).
- 📋 Audit logging & 📊 metrics — append-only audit trail with identity context; Prometheus
/metricsendpoint. - 🧠 Memory safety — secrets zeroized; constant-time comparisons for tokens and passphrases.
# Toolchain: Rust 1.70+. Build dependencies (Debian/Ubuntu):
sudo apt-get install -y protobuf-compiler pkg-config clang libclang-dev \
nettle-dev libgmp-dev libssl-dev
# Falcon sources are vendored C code in `src/crypto/falcon/falcon_c/`# Build (daemon, CLI, and the PKCS#11 module libsoftkms.so)
cargo build --release
# Start the daemon — it boots LOCKED (no key ops until unlocked)
./target/release/softkms-daemon --foreground &
# First boot: initialize with an admin passphrase (prompts)
./target/release/softkms init
# Create a client identity (for services / agents) — save the token!
./target/release/softkms identity create --type ai-agent
# Generate a key, isolated to that identity
./target/release/softkms --token <token> generate --algorithm ed25519 --label mykey
# Sign data
./target/release/softkms --token <token> sign --label mykey --data "Hello World"The daemon boots locked and keeps no secret on disk or in the environment. Key operations are
rejected until an operator unlocks it over the local admin channel (loopback gRPC, reached
directly or via docker exec):
softkms init # first boot only — sets the admin passphrase (prompts)
softkms unlock # after every restart — loads the master key for the session (prompts)
softkms lock # clear the in-memory master key
softkms health # shows initialized + unlocked (readiness)- Liveness vs readiness:
softkms-daemon --health-checkreports liveness; readiness is theunlockedfield on the gRPCHealthresponse and RESTGET /v1/status. - This is the LUKS / Vault-manual-unseal posture: a restart leaves the daemon locked until a human (or an authenticated step) unlocks it. Unattended auto-unseal (TPM2 / cloud-KMS) is on the roadmap.
softKMS uses ECC public keys for identity and isolates access between clients:
- Admin (passphrase) — full access to all keys.
- Clients (bearer token) — access only to keys they own; namespace-isolated.
# Create an Ed25519 identity (default)
softkms identity create --type ai-agent --description "Trading Bot"
# -> Public Key: ed25519:... Token: <SAVE THIS — shown once>
# Use the token (flag or env var)
export SOFTKMS_TOKEN="..."
softkms --token "$SOFTKMS_TOKEN" list
# PKCS#11 (token as PIN)
pkcs11-tool --module target/release/libsoftkms.so --login --pin "<token>" --list-objectsSee Identity Management for details.
Symmetric AES-256-GCM keys are deterministically derived from a BIP39 seed (HKDF-SHA512 over a label path), so the entire keystore — signing keys and symmetric keys — is recoverable from the mnemonic. A fresh random key can be generated instead when reproducibility is not wanted.
# Import a seed, then derive a symmetric key along a label path
softkms --token <token> import-seed --mnemonic "word1 word2 ... word12" --label wallet
softkms --token <token> derive-symmetric --seed wallet -P "m/sym/app/db-key" --label dbkey
# Encrypt / decrypt (ciphertext is base64 of nonce||ciphertext||tag)
CT=$(softkms --token <token> encrypt --label dbkey --data "top secret" --aad "context")
softkms --token <token> decrypt --label dbkey --ciphertext "$CT" --aad "context"
# Or a non-recoverable random key
softkms --token <token> generate-symmetric --label ephemeralThe AES key never leaves the daemon; a fresh 96-bit nonce is generated per encryption.
softKMS is privilege-separated into two processes (one systemd unit, same user): a network-facing frontend that holds no keys, and a keykeeper that holds the master key and does all crypto.
flowchart TB
subgraph "Clients"
CLI[CLI]
PKCS[PKCS#11 Module]
APP[Apps / Agents]
end
subgraph "frontend process (no keys)"
REST[REST API<br/>0.0.0.0:8080]
end
subgraph "keykeeper process (holds keys)"
GRPC[gRPC admin<br/>127.0.0.1:50051 / unix]
KEYS[Key Service &<br/>Crypto Engines]
SEC[Security Manager<br/>locked/unlocked]
end
STORE[(Encrypted File Storage)]
APP --> REST
PKCS --> REST
CLI --> GRPC
REST -->|gRPC + bearer token| GRPC
GRPC --> KEYS
KEYS --> SEC
SEC --> STORE
- keykeeper holds the master key, storage, and all crypto; serves the gRPC admin channel
(
init/unlock/lock+ key ops) and spawns/supervises the frontend. - frontend is the REST (0.0.0.0:8080) client surface (optionally TLS). It holds no key material — each request is forwarded to the keykeeper over gRPC with the caller's token.
- A compromise of the network-facing frontend can't reach the keys: separate process, and the keykeeper
is
PR_SET_DUMPABLE=0(no same-userptrace//proc/mem, no core dumps). - Keys never leave the keykeeper; clients receive only results (signatures, ciphertext).
git clone https://github.com/ehanoc/softKMS.git
cd softKMS
git submodule update --init --recursive
cargo build --release
sudo cp target/release/softkms-daemon /usr/local/bin/
sudo cp target/release/softkms /usr/local/bin/The daemon runs in the foreground and is backgrounded + supervised by systemd (it does not self-fork).
The shipped unit (pkg/systemd/softkms.service, also installed by the Debian package) is Type=notify:
the daemon signals readiness via sd_notify once it is listening, so systemctl start returns promptly.
sudo cp pkg/systemd/softkms.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now softkms # boots LOCKED; comes up active (running)
softkms init # first boot only
softkms unlock # after each restartThe service reaches active (running) as soon as it is listening, but boots locked — it is not
ready to serve key operations until softkms unlock (readiness = the unlocked field on
GET /v1/status; liveness = softkms-daemon --health-check).
The image ships the daemon, the CLI, and libsoftkms.so. It boots locked; you initialize and
unlock via docker exec. Only the REST API is published; the gRPC admin channel stays internal.
docker compose -f docker/docker-compose.yml up -d # boots LOCKED
docker compose exec -it softkms softkms init # first boot only (prompts)
docker compose exec -it softkms softkms unlock # after each restart (prompts)
docker compose exec softkms softkms-daemon --health-checkThe REST API is available on 127.0.0.1:8080. Persist /var/lib/softkms (keystore, .salt,
identities, audit log). For network exposure, enable TLS or front it with a TLS-terminating proxy.
See docs/OPERATIONS.md for backup/restore and the full lifecycle.
Configured via config.toml (see config.toml.example) or environment variables:
| Variable | Purpose |
|---|---|
SOFTKMS_GRPC_ADDR / SOFTKMS_REST_ADDR |
Bind addresses |
SOFTKMS_STORAGE_PATH |
Keystore directory |
SOFTKMS_TLS_CERT / SOFTKMS_TLS_KEY |
Enable REST TLS (PEM cert + key) |
SOFTKMS_TLS_CLIENT_CA |
Client CA bundle — enables REST mTLS (require client certs) |
SOFTKMS_ALLOW_INSECURE_BIND |
Allow a non-loopback bind without TLS (1 to override the fail-safe) |
SOFTKMS_LOG_FORMAT |
json for structured JSON logging; default is plain text |
These are read by the daemon. There is deliberately no passphrase variable — the daemon is
unlocked interactively. The CLI takes its identity token via the --token/-t flag (it does not read
an environment variable); pass it explicitly, e.g. softkms --token "$MY_TOKEN" list.
# Lifecycle
softkms init # first boot
softkms unlock # after each restart
softkms health
# Keys (admin uses -p/prompt; clients use --token)
softkms --token <t> generate --algorithm ed25519 --label mykey
softkms --token <t> generate --algorithm falcon512 --label pq-key
softkms --token <t> list
softkms --token <t> sign --label mykey --data "message"
softkms --token <t> verify --label mykey --data "message" --signature "..."
# HD wallet
softkms --token <t> import-seed --mnemonic "word1 ... word12" --label wallet
softkms --token <t> derive --algorithm ed25519 --seed wallet --path "m/44'/283'/0'/0/0" --label child
# Symmetric encryption
softkms --token <t> derive-symmetric --seed wallet -P "m/sym/app/key" --label dbkey
softkms --token <t> encrypt --label dbkey --data "secret" --aad "ctx"
softkms --token <t> decrypt --label dbkey --ciphertext "<base64>" --aad "ctx"
# Export (admin)
softkms export-ssh --label mykey --output ~/.ssh/id_ed25519
softkms export-gpg --label mykey --user-id "User <user@example.com>"
# PKCS#11
pkcs11-tool --module target/release/libsoftkms.so --list-mechanisms
pkcs11-tool --module target/release/libsoftkms.so --login --pin "<token>" \
--keypairgen --key-type EC:prime256v1 -m 0x1040| Component | Location | Description |
|---|---|---|
| CLI | cli/src/main.rs |
Command-line client |
| gRPC / REST API | src/api/ (grpc.rs, rest.rs, client.rs) |
Keykeeper gRPC + REST gateway |
| Frontend | src/frontend.rs |
Key-free REST frontend process (privilege separation) |
| Identity | src/identity/ |
Token-based identities & isolation |
| Key Service | src/key_service.rs |
Key lifecycle (wrap/unwrap) |
| Security | src/security/ |
Master key, AES-GCM wrapping, locked/unlocked state |
| Crypto | src/crypto/ (ed25519.rs, p256.rs, hd_ed25519.rs, symmetric.rs) |
Signing + symmetric engines |
| Falcon PQC | src/crypto/falcon/ |
Post-quantum signatures (C FFI) |
| PKCS#11 | src/pkcs11/ |
PKCS#11 provider (REST client) |
| Storage | src/storage/ |
Encrypted file storage |
| Audit | src/audit/ |
Audit logging |
cargo build --all-targets # CI builds with -D warnings
cargo test --workspace # daemon/PKCS#11 e2e tests self-skip without a release build
cargo build --release # then `cargo test --workspace --release` runs the e2e testsSee CONTRIBUTING.md. CI runs build + tests, cargo audit, and a Docker build/scan.
Version: 0.2.0 — see CHANGELOG.md.
Implemented
- ✅ Daemon with gRPC, REST, and CLI; PKCS#11 provider
- ✅ Locked-boot + unlock model; readiness/health checks
- ✅ Ed25519, P-256, Falcon-512/1024 signing
- ✅ BIP32/44 HD derivation; deterministic P-256; xpub import
- ✅ Seed-derived AES-256-GCM symmetric encryption
- ✅ Identity isolation with bearer tokens; per-token expiry (
--expires-in-days) - ✅ Passphrase rotation (transactional cross-namespace re-key) and key rotation
- ✅ Encrypted file storage; audit logging;
/metrics - ✅ Optional REST TLS and mTLS (client-cert) enforcement; Docker image + Debian packaging
- ✅ Privilege separation: key-free REST frontend process + keykeeper (memory isolation via
PR_SET_DUMPABLE); hardened systemd sandbox,Type=notify+ watchdog, UDS admin channel
Roadmap / not yet implemented
- 🔭 TPM2 / cloud-KMS auto-unseal for unattended restart
- 🔭 WebAuthn (design only)
- 🔭 Independent security audit before any production claim
- AES-256-GCM key wrapping at rest; PBKDF2 (210k iterations) master key.
- Locked-boot with no persisted secret; constant-time token/passphrase comparison.
- Identity isolation — clients access only their own keys.
- Zeroization of sensitive material; keys never leave the daemon.
Known limitations and the threat model are documented in docs/SECURITY.md; backup/restore and operations in docs/OPERATIONS.md.
- Usage Guide · Identity Management · Architecture · Security Model · API Reference · Operations · PKCS#11 Mechanisms · Alerting
- Changelog · Contributing
Contributions are welcome — see CONTRIBUTING.md.
AGPL-3.0 — see LICENSE.
- 📖 Documentation · 🐛 Issues ·