Signing
TKeeper provides threshold signing using two protocols:
- GG20 (ECDSA, secp256k1)
- FROST (Schnorr, Ed25519 and secp256k1)
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 + SECP256K1 |
65 bytes | Compact ECDSA signature including recId (recovery id). |
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 + 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",
"operations": {
"op1": "base64(message-bytes)"
},
"context": {
"kind": "BIP340"
},
"hash": false
}
Fields:
| Field | Type | Required | Meaning |
|---|---|---|---|
keyId |
string | yes | Key logicalId. |
algorithm |
enum | yes | GG20 or FROST. |
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. |
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",
"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). |
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. - 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 - Message MUST be 32 bytes in length, so either hash yourself, or set
hash = truewhile signing.