Skip to content

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 include merkleRoot64 (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 sigType matches the actual encoding returned by /sign.
  • If hash = true, before signing/verification server hashes the message first SHA-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; set merkleRoot64 only 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 = true while signing.