softKMS exposes two network APIs plus a CLI:
| Surface | Default address | Audience | Auth |
|---|---|---|---|
| gRPC | 127.0.0.1:50051 |
Local admin channel (init/unlock/lock, admin + client ops) |
Admin passphrase or identity token |
| REST | 0.0.0.0:8080 |
Client surface (token-scoped) | Identity token only (export endpoints also take an admin passphrase in the body) |
Keys never leave the daemon; clients receive only results (signatures, ciphertext, public keys). The gRPC channel is intended to stay loopback-only; the REST channel can be exposed (enable TLS).
The canonical schema for the gRPC API is proto/softkms.proto — message
field names/types are defined there and are the source of truth. This document describes the surface,
auth model, and REST payloads.
Created with softkms identity create (shown once). A token authorizes operations scoped to that
identity's own keys (namespace isolation). The server stores only SHA256(token).
gRPC requests carry flat fields on the request message (there is no AuthContext/oneof):
auth_token— an identity token (scopes the request to that identity), and/orpassphrase— the admin passphrase (full access).
If auth_token is non-empty it is used; otherwise the request is treated as admin and the
passphrase is verified on every call (an empty/wrong passphrase is rejected with
PERMISSION_DENIED, even while the daemon is unlocked).
REST is token-only. Pass the identity token via either header:
Authorization: Bearer <token>
# or
X-SoftKMS-Token: <token>
Exceptions: GET /health and GET /v1/status are unauthenticated; GET /metrics
requires an identity token or admin passphrase (gated via validate_auth).
POST /v1/keys/:id/verify authenticates optionally; the two export/* endpoints additionally
require an admin_passphrase in the JSON body.
ed25519, p256, falcon512, falcon1024.
RSA and ECDSA-secp256k1 are not implemented (the proto
Algorithmenum reserves the names, but create/sign/verify reject them). Falcon keys cannot be HD-derived from a seed.
Base URL: http://127.0.0.1:8080 (or https://… when TLS is enabled). All request/response bodies are
JSON. Binary fields (data, signature, ciphertext, plaintext, aad) are base64-encoded.
| Method | Path | Description | Response |
|---|---|---|---|
| GET | /health |
Liveness | 200 body OK |
| GET | /v1/status |
Readiness (no topology disclosed) | { "version", "status", "unlocked" } |
| GET | /metrics |
Prometheus exposition | text format |
curl -s http://127.0.0.1:8080/v1/status
# {"version":"0.2.0","status":"ok","unlocked":true}| Method | Path | Body | Response |
|---|---|---|---|
| GET | /v1/keys |
— | { "keys": [KeyInfo, …] } |
| POST | /v1/keys |
{ "algorithm", "label"? } |
{ "key_id", "algorithm", "public_key"?, "created_at", "label" } |
| GET | /v1/keys/:id |
— | KeyInfo (owner-checked) |
| POST | /v1/keys/:id/sign |
{ "data": base64 } |
{ "key_id", "signature": base64, "algorithm" } |
| POST | /v1/keys/:id/verify |
{ "data": base64, "signature": base64 } |
{ "valid": bool, "algorithm" } |
| POST | /v1/keys/:id/encrypt |
{ "data": base64, "aad"?: base64 } |
{ "ciphertext": base64 } |
| POST | /v1/keys/:id/decrypt |
{ "ciphertext": base64, "aad"?: base64 } |
{ "plaintext": base64 } |
| POST | /v1/keys/:id/export/ssh |
{ "passphrase", "admin_passphrase", "output_path"? } |
{ "key_id", "output_path", "algorithm" } |
| POST | /v1/keys/:id/export/gpg |
{ "admin_passphrase", "user_id"? } |
{ "key_id", "user_id", "algorithm", "armored_key" } |
KeyInfo = { "key_id", "algorithm", "key_type", "label", "created_at", "public_key"?, "owner_identity"? }.
For encrypt/decrypt the ciphertext is base64 of nonce || ciphertext || tag (AES-256-GCM, fresh
96-bit nonce per message).
| Method | Path | Body | Response |
|---|---|---|---|
| POST | /v1/symmetric-keys |
{ "label"? } |
{ "key_id", "label", "algorithm" } (random AES-256-GCM key) |
| POST | /v1/seeds/:id/derive-symmetric |
{ "path", "label"? } |
{ "key_id", "label", "algorithm" } (deterministic, seed-recoverable) |
| Method | Path | Response |
|---|---|---|
| GET | /v1/identities/me |
{ "public_key", "key_type", "client_type", "description"?, "created_at", "last_used", "is_active" } |
TOKEN="<identity-token>"
H="Authorization: Bearer $TOKEN"
# Create an Ed25519 key
curl -s -H "$H" -H 'Content-Type: application/json' \
-d '{"algorithm":"ed25519","label":"mykey"}' \
http://127.0.0.1:8080/v1/keys
# -> {"key_id":"<uuid>","algorithm":"ed25519","public_key":"<b64>","created_at":"…","label":"mykey"}
# Sign (data is base64 — here base64("hello"))
curl -s -H "$H" -H 'Content-Type: application/json' \
-d "{\"data\":\"$(printf 'hello' | base64)\"}" \
http://127.0.0.1:8080/v1/keys/<uuid>/sign
# -> {"key_id":"<uuid>","signature":"<b64>","algorithm":"ed25519"}
# List the identity's own keys
curl -s -H "$H" http://127.0.0.1:8080/v1/keysTwo services (see proto/softkms.proto for message schemas):
Lifecycle: Init, Unlock, Lock, Health.
Keys: CreateKey, ListKeys, GetKey, DeleteKey, Sign, Verify, RotateKey,
ChangePassphrase.
Symmetric: Encrypt, Decrypt, DeriveSymmetricKey, GenerateSymmetricKey.
HD / seeds: ImportSeed, DeriveP256, DeriveEd25519, ImportXpub, DerivePublic.
Export: ExportSshKey, ExportGpgKey.
Backup / restore: Backup, Restore.
Metrics: GetMetrics.
DeriveKeyexists in the proto but is not implemented — it returnsUNIMPLEMENTED. UseDeriveEd25519/DeriveP256instead.BackupandRestorerequire admin authentication and write/read a tar.gz tarball to/from a filesystem path.
CreateIdentity, ListIdentities, GetIdentity, RevokeIdentity.
HealthResponse includes healthy, version, storage_ready, api_ready, initialized, and
unlocked (readiness). Init/Health are RPCs on KeyStore (there is no separate health service).
# Health (no auth)
grpcurl -plaintext 127.0.0.1:50051 softkms.KeyStore/Health
# Create a key as admin (passphrase in the request message)
grpcurl -plaintext -d '{"algorithm":"ed25519","label":"mykey","passphrase":"<admin>"}' \
127.0.0.1:50051 softkms.KeyStore/CreateKey
# Create a key as an identity (auth_token instead of passphrase)
grpcurl -plaintext -d '{"algorithm":"ed25519","label":"mykey","auth_token":"<token>"}' \
127.0.0.1:50051 softkms.KeyStore/CreateKey(Server reflection availability depends on build; otherwise pass -import-path proto -proto softkms.proto.)
gRPC handlers map domain errors to Status; REST maps them to HTTP codes:
| Condition | gRPC | HTTP |
|---|---|---|
| Bad/empty admin passphrase, ownership denied | PERMISSION_DENIED |
401/403 |
| Key/identity not found | NOT_FOUND |
404 |
| Invalid argument (bad key id, unsupported algorithm) | INVALID_ARGUMENT |
400 |
| Keystore not initialized / locked; non-signing key for verify | FAILED_PRECONDITION |
400/412 |
| Missing/invalid token (REST) | — | 401 |
Error messages avoid disclosing key material; /v1/status deliberately omits bind addresses and the
storage backend.
- USAGE.md — CLI guide · IDENTITIES.md — identities & tokens
- proto/softkms.proto — gRPC schema (source of truth)
- OPERATIONS.md — deployment, TLS/mTLS, backup/restore