TKeeper provides threshold signing through two protocol families:
| Protocol | Signature family | Curve support |
|---|---|---|
GG20 |
ECDSA | P256, SECP256K1 |
FROST |
Schnorr | ED25519, P256 (standard mode), SECP256K1 (standard mode and BIP-340/Taproot context) |
The signing API is protocol-agnostic: the request selects the signing protocol via algorithm, and the server returns the resulting signature type in the response.
Supported signature types
TKeeper can return one of the following signature encodings:
| Signature type | Produced by | Output size | Notes |
|---|---|---|---|
ECDSA |
GG20 + P256 or GG20 + SECP256K1 |
65 bytes | Compact ECDSA signature. For secp256k1, response semantics include recovery-id compatible output. |
SCHNORR |
FROST + ED25519 or FROST + SECP256K1 in BIP-340/Taproot context |
64 bytes | Standard 64-byte Schnorr signature (r | | s). For secp256k1, this corresponds to x-only Schnorr semantics used by BIP-340/Taproot. |
SEC1R65SCHNORR |
FROST + P256 or FROST + SECP256K1 (standard mode) |
65 bytes | SEC1-style Schnorr where R is encoded in full form, producing a 65-byte encoding. |
The response includes type so clients can interpret the returned bytes correctly.
Threshold signing endpoint
- POST
/v1/keeper/sign - Permission:
tkeeper.key.<logicalId>.sign
Request model
{
"keyId": "btc-hot-wallet",
"algorithm": "FROST",
"tweak": "my-user-id",
"operations": {
"op1": "base64(message-bytes)"
},
"context": {
"kind": "BIP340"
},
"hash": false,
"approvals": {
"keeperId": 1,
"nonce": "ops-2026-02-12-010",
"timestamp": 1764419000123,
"proofs": [
{
"fingerprint": "base64(sha256(public-key-bytes))",
"signature64": "base64(signature-over-request-hash)"
}
]
}
}
Fields:
| Field | Type | Required | Meaning |
|---|---|---|---|
keyId |
string | yes | Key logicalId. |
algorithm |
enum | yes | GG20 or FROST. |
tweak |
string | no | Optional tweak input used to derive a child key from the base key for this request. Example: "my-user-id". |
operations |
object(map) | yes | Map operationId -> data64 (base64 message bytes). |
context |
object | no | Signature context used for secp256k1 + FROST (see below). |
hash |
boolean | no | If true, TKeeper SHA-256 prehashes the message bytes before signing. Default is false. |
approvals |
object | no | Required when four-eye control is active for the key. |
Four-eye approvals (when enabled)
If the key has policy.fourEye, /v1/keeper/sign must include approvals with at least m valid proofs.
For approval schema, keeperId semantics, canonical payload signing rules, and replay/availability trade-offs, see:
Key Tweaking (optional)
tweak allows deterministic key derivation relative to the base key identified by keyId.
This lets you keep one master key while deriving isolated child keys per tenant, user, or workflow input.
Warning
tweakin TKeeper is not Taproot tweaking and not BIP key derivation.
It is an application-level key derivation input for threshold operations.
Note
- For GG20, only one operation is supported per request (by design).
- For FROST, multiple operations may be submitted in a single request (each operation key is signed independently).
Signature context (secp256k1 + FROST)
When signing with FROST using a secp256k1 key, you may specify an explicit context:
BIP340: produce x-only Schnorr signature semantics compatible with BIP-340 usage.TAPROOT: produce Taproot-context signature semantics; optionally includemerkleRoot64(32 bytes, base64).
BIP-340 context
{
"kind": "BIP340"
}
Taproot context
{
"kind": "TAPROOT",
"merkleRoot64": "base64(32-byte-merkle-root)"
}
If merkleRoot64 is omitted, it is treated as not set.
Reference documents:
https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki
https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki
Response model
{
"code": "SUCCESS",
"type": "SCHNORR",
"signature": {
"op1": "base64(signature-bytes)"
},
"generation": 3
}
Fields:
| Field | Meaning |
|---|---|
code |
SUCCESS or FAILED. |
type |
Signature type (ECDSA, SCHNORR, SEC1R65SCHNORR). |
signature |
Map operationId -> signature64 (nullable on failure). |
generation |
Key generation used (or attempted generation in failure paths). |
Signature verification endpoint
- POST
/v1/keeper/sign/verify - Permission:
tkeeper.key.<logicalId>.verify
Request model
{
"keyId": "btc-hot-wallet",
"sigType": "SCHNORR",
"tweak": "my-user-id",
"data64": "base64(message-bytes)",
"signature64": "base64(signature-bytes)",
"context": { "kind": "BIP340" },
"generation": 3,
"hash": false
}
Fields:
| Field | Type | Required | Meaning |
|---|---|---|---|
keyId |
string | yes | Key logicalId. |
sigType |
enum | yes | Signature type to verify (ECDSA, SCHNORR, SEC1R65SCHNORR). |
tweak |
string | no | Optional tweak input. If signing used tweak, verification must use the exact same value. |
data64 |
string | yes | Base64 message bytes. |
signature64 |
string | yes | Base64 signature bytes. |
context |
object | no | BIP-340 / Taproot context for secp256k1 + FROST verification. |
generation |
int | no | Verify against a specific generation. If omitted, the current generation is used. |
hash |
boolean | no | If true, TKeeper SHA-256 prehashes the message bytes before verification. Default is false. |
Response model
{
"valid": true
}
Notes and common pitfalls
- Before verification, ensure that
sigTypematches the actual encoding returned by/sign. - If
hash = true, before signing/verification server hashes the message firstSHA-256(messageBytes). This must be consistent across both operations. - If you sign with
tweak, you must verify with the sametweak. - When using secp256k1 + FROST:
- use
context.kind = "BIP340"for x-only Schnorr workflows that expect BIP-340 semantics - use
context.kind = "TAPROOT"for Taproot-context workflows; setmerkleRoot64only when you need to bind signature semantics to a specific Taproot merkle root
- use
- Message MUST be 32 bytes in length, so either hash yourself, or set
hash = truewhile signing.