Skip to content

ehanoc/softKMS

softKMS — Modern Software Key Management System

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.

License: AGPL-3.0 Rust CI Version

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.

Table of Contents

Why softKMS?

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

Key Features

  • 🔐 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 /metrics endpoint.
  • 🧠 Memory safety — secrets zeroized; constant-time comparisons for tokens and passphrases.

Quick Start

Prerequisites

# 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 & run

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

Unlock Lifecycle

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-check reports liveness; readiness is the unlocked field on the gRPC Health response and REST GET /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.

Identity-Based Access Control

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-objects

See Identity Management for details.

Symmetric Encryption

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 ephemeral

The AES key never leaves the daemon; a fresh 96-bit nonce is generated per encryption.

Architecture

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
Loading
  • 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-user ptrace//proc/mem, no core dumps).
  • Keys never leave the keykeeper; clients receive only results (signatures, ciphertext).

Installation

From source

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/

Run as a systemd service

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 restart

The 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).

Docker

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-check

The 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.

Configuration

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.

Quick Commands

# 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

Project Structure

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

Development

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 tests

See CONTRIBUTING.md. CI runs build + tests, cargo audit, and a Docker build/scan.

Project Status

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

Security

  • 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.

Documentation

Contributing

Contributions are welcome — see CONTRIBUTING.md.

License

AGPL-3.0 — see LICENSE.

Support

Releases

No releases published

Packages

 
 
 

Contributors

Languages