Skip to content

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 is 3, generation 1 may be destroyed, but generation 2 must 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: MILLISECONDS or SECONDS
  • notAfter: 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 identifier
  • version: 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: APPLY or PROCESS
  • logicalId: keyset logical id
  • generation: keyset generation the expiration refers to
  • expiresAt: 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): apply or process
  • limit (optional): default 100, clamped to [1..2000]
  • cursor (optional): pagination cursor
  • Either:
  • windowSec (required if no to): window size in seconds, relative to UTC now
  • or
  • to (required if no windowSec): end epoch seconds
    • from (optional): start epoch seconds, defaults to 0

Validation rules:

  • Missing type -> MISSING_EXPIRE_TYPE
  • Invalid type (not apply/process) -> INVALID_EXPIRE_TYPE
  • If neither windowSec nor to is 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): default 100, 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): default 100, 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): apply or process
  • limit (optional): default 100, 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 of ExpireItem
  • next: cursor for the next page, or null when 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.