Key lifecycle
Tip
You can manage keys in the TKeeper control plane available at /ui.
TKeeper represents each logical key as a stable logicalId with an integer generation. Lifecycle operations are modeled as explicit state transitions and are enforced by permissions and safety rules.
Model
- logicalId: stable identifier for a logical key (example:
btc-hot-wallet). - generation: integer version of a logical key.
- current generation: the active generation used for new cryptographic output (signing/encryption).
TKeeper can maintain historical generations for verification/decryption and for operational rollback.
Lifecycle operations
Warning
ALL TKeeper nodes should be available during CREATE, ROTATE and REFRESH operations.
At least THRESHOLD of nodes must be available for DESTROY operation.
If more than t but not all TKeeper instances were available for DESTROY operation, you'll receive a warning in the Warning header and a 299 HTTP status code.
CREATE
Creates a new logical key using Distributed Key Generation (DKG). No single keeper generates or holds the full private key. The result is stored as generation 1 for the new logicalId.
API: POST /v1/keeper/dkg with mode = "CREATE"
Permission: tkeeper.dkg.create
ROTATE
Creates a new key generation and makes it the current generation for the same logicalId. Rotation is used for planned key replacement and cryptographic hygiene.
Operational effect:
- a new generation becomes current
- older generations become historical
API: POST /v1/keeper/dkg with mode = "ROTATE"
Permission: tkeeper.dkg.rotate
REFRESH
Replaces secret shares while preserving continuity of the logical key. Refresh is designed for cases where you want to invalidate potentially exposed shares without changing the operational identity of the key.
Tip
Key refresh re-randomizes the secret shares across the keepers without changing the underlying private key.
The logical key identity stays the same, and the public key stays the same.
If an attacker previously stole one or more keeper shares, a refresh makes those stolen shares cryptographically useless: they no longer match the refreshed share set, so they cannot be combined to reach the threshold for signing or decryption.
API: POST /v1/keeper/dkg with mode = "REFRESH"
Permission: tkeeper.dkg.refresh
DESTROY
Permanently destroys key material for a specific generation of a logicalId. Destroy is intentionally constrained to reduce operational risk.
Safety rule:
- destroy is permitted only for generations that are at least two generations older than the current generation for the same
logicalId
Example: if current generation is3, generation1may be destroyed, but generation2must not be destroyed.
API: POST /v1/keeper/destroy
Permission: tkeeper.key.<keyId>.destroy
DKG API
DKG is the unified entry point for CREATE / ROTATE / REFRESH.
Endpoint
- POST
/v1/keeper/dkg
Request body
{
"keyId": "btc-hot-wallet",
"curve": "SECP256K1",
"mode": "CREATE",
"policy": {
"apply": { "unit": "SECONDS", "notAfter": 1764417207 },
"process": { "unit": "SECONDS", "notAfter": 1767019207 },
"allowHistoricalProcess": true
}
}
Fields:
| Field | Type | Required | Meaning |
|---|---|---|---|
keyId |
string | yes | Logical key identifier (logicalId). |
curve |
enum | yes | ED25519 or SECP256K1. |
mode |
enum | yes | CREATE, ROTATE, REFRESH. |
policy |
object | no | Optional KeySetPolicy applied to this logical key. |
assetOwner |
string | no | Optional operational note |
Response:
- returns HTTP 200 with no response body on success.
Public key retrieval
You can retrieve the public key for a logical key, optionally specifying a generation.
- GET
/v1/keeper/publicKey?keyId=<logicalId>&generation=<int?> - Permission:
tkeeper.key.<keyId>.public
Response:
{
"data64": "<base64-public-key-bytes>"
}
KeySetPolicy
KeySetPolicy is an optional policy attached to a logical key. It controls whether the key may be used for:
- apply operations: create new artifacts using key material (signing, encryption)
- process operations: consume/validate existing artifacts (verification, decryption)
Policy fields
| Field | Meaning |
|---|---|
apply.notAfter |
Optional deadline after which apply operations are denied. |
process.notAfter |
Optional deadline after which process operations on historical generations are denied. |
allowHistoricalProcess |
If false, denies process operations on historical generations regardless of deadlines. Default is true. |
Timestamp format
Both apply and process use the NotAfter structure:
{ "unit": "MILLISECONDS", "notAfter": 1764417207123 }
unit:MILLISECONDSorSECONDSnotAfter: integer timestamp in the chosen unit (nullable)
Policy constraints
If both apply.notAfter and process.notAfter are provided:
- process.notAfter must be later than apply.notAfter, so data created before apply is blocked can still be verified/decrypted for a retention period.
Operational warning:
- setting process.notAfter or allowHistoricalProcess = false can make older signatures impossible to verify and older ciphertexts impossible to decrypt once the deadline passes (or immediately, if historical processing is disabled).
Key status semantics
Inventory and key listing endpoints report a status per key view.
| Status | Meaning |
|---|---|
ACTIVE |
Key is usable, subject to permissions and policy. |
APPLY_EXPIRED |
Apply is no longer allowed (for example, generation is historical or apply deadline passed), but process may still be allowed. |
EXPIRED |
Neither apply nor historical processing is allowed. |
DISABLED |
Key is administratively disabled (deny apply and process). |
DESTROYED |
Key generation material has been destroyed. |
Destroy API
Warning
You can destroy a key only if it has at least two generations older than the current generation. (e.g. current generation is 5, then 1, 2, 3 are allowed to be destroyed)
Destroy is separate from DKG and targets a specific generation.
Endpoint
- POST
/v1/keeper/destroy
Request body
{
"keyId": "btc-hot-wallet",
"version": 1
}
Fields:
keyId: logical key identifierversion: generation to destroy
Response:
- returns HTTP 200 with no response body on success.
Expiration
TKeeper exposes a key expiration query API that lets external systems poll for keysets that are about to expire (or already expired), without storing “alerts” inside TKeeper. You pull ExpireItem records and decide in your own system how to page, dedupe, escalate, and notify.
Model
ExpireItem represents a single expiration event for a keyset generation.
{
"type": "APPLY",
"logicalId": "my-key-id",
"generation": 2,
"expiresAt": 1766591893
}
Fields:
type:APPLYorPROCESSlogicalId: keyset logical idgeneration: keyset generation the expiration refers toexpiresAt: UNIX epoch seconds
Permissions
All endpoints require:
tkeeper.expired.view
If missing, the server returns ACCESS_DENIED.
Endpoints
List expiring keys by window or range
GET /v1/keeper/expires
Use this when you want a single endpoint that supports either:
- a window relative to “now”, or
- an explicit time range.
Query params:
type(required):applyorprocesslimit(optional): default100, clamped to[1..2000]cursor(optional): pagination cursor- Either:
windowSec(required if noto): window size in seconds, relative to UTCnow- or
to(required if nowindowSec): end epoch secondsfrom(optional): start epoch seconds, defaults to0
Validation rules:
- Missing
type->MISSING_EXPIRE_TYPE - Invalid
type(notapply/process) ->INVALID_EXPIRE_TYPE - If neither
windowSecnortois provided ->MISSING_WINDOW
Examples:
Window query (next 30 days apply expirations):
curl -G "$BASE_URL/v1/keeper/expires" \
-H "X-DEV-TOKEN: $TOKEN" \
--data-urlencode "type=apply" \
--data-urlencode "windowSec=2592000" \
--data-urlencode "limit=200"
Range query (process expirations between two timestamps):
curl -G "$BASE_URL/v1/keeper/expires" \
-H "X-DEV-TOKEN: $TOKEN" \
--data-urlencode "type=process" \
--data-urlencode "from=1764000000" \
--data-urlencode "to=1766592000" \
--data-urlencode "limit=200"
List apply expirations in a window
GET /v1/keeper/expires/apply
Query params:
windowSec(required)limit(optional): default100, clamped to[1..2000]cursor(optional)
Validation rules:
- Missing
windowSec->MISSING_WINDOW
Example:
curl -G "$BASE_URL/v1/keeper/expires/apply" \
-H "X-DEV-TOKEN: $TOKEN" \
--data-urlencode "windowSec=604800" \
--data-urlencode "limit=100"
List process expirations in a window
GET /v1/keeper/expires/process
Query params:
windowSec(required)limit(optional): default100, clamped to[1..2000]cursor(optional)
Validation rules:
- Missing
windowSec->MISSING_WINDOW
Example:
curl -G "$BASE_URL/v1/keeper/expires/process" \
-H "X-DEV-TOKEN: $TOKEN" \
--data-urlencode "windowSec=604800" \
--data-urlencode "limit=100"
List expired keys
GET /v1/keeper/expires/expired
This endpoint returns items that are already expired as of server now.
Query params:
type(required):applyorprocesslimit(optional): default100, clamped to[1..2000]cursor(optional)
Validation rules:
- Missing
type->MISSING_EXPIRE_TYPE - Invalid
type->INVALID_EXPIRE_TYPE
Example:
curl -G "$BASE_URL/v1/keeper/expires/expired" \
-H "X-DEV-TOKEN: $TOKEN" \
--data-urlencode "type=process" \
--data-urlencode "limit=200"
Pagination
All expiration endpoints return Page<ExpireItem, String>:
items: list ofExpireItemnext: cursor for the next page, ornullwhen done
Clients should keep requesting pages until next == null.
Note
TKeeper does not track “notification stages” (30d/7d/1d). The service returns raw expiration times, and external systems decide their own thresholds, escalation logic, and idempotency tracking.