---
title: "审批回调"
description: "使用 RS256 签名的 JWT，在你自己的服务器上批准或拒绝每一项 Cosigner 操作。"
url: https://docs.fystack.io/zh/cosigner/approval-callback
last_updated: 2026-11-13
---

> ## Documentation index
> Fetch the complete documentation index at: https://docs.fystack.io/llms.txt
> Use it to discover all available pages before exploring further.

# 审批回调

在加入任何会话之前，Cosigner 都会向你的服务器发送一个已签名的请求，并等待已签名的 `approve`。你的服务器会拿到待签名的原始字节，因此可以独立于 Fystack 验证交易。

_Diagram: Cosigner 向回调发送已签名的请求，回调返回 approve 或 reject_

## 工作原理

1. Cosigner 验证 Orchestrator 发来的 `session_start`。
2. Cosigner 向你的 URL `POST` 一个 RS256 JWT（`Content-Type: application/jwt`）。
3. 你的服务器验证该 JWT，执行策略，然后返回 `200` 以及一个 RS256 JWT。
4. Cosigner 验证响应和 `request_id`，然后加入会话。

`KEYGEN`、`SIGN` 和 `RESHARE` 都会发送请求。请对 `SIGN` 应用交易策略。

> **Note:** Cosigner 采用失败即拒绝（fail closed）原则。拒绝、非 200 响应、重定向、超时、响应体过大、签名错误或 `request_id` 不匹配，都意味着不会签名。会话将以 `CALLBACK_REJECTED` 失败。

## 配置

需要两对 RSA-4096 密钥对。双方只交换公钥。

- **Cosigner 密钥**：由 `cosigner-cli` 创建，用于签署请求。将 `cosigner-public.pem` 提供给你的服务器。
- **服务器密钥**：在你的服务器上或 KMS 中创建，用于签署响应。将 `server-public.pem` 提供给 Cosigner。

**同一宿主机：** 指向服务器的密钥文件夹。CLI 会读取 `server-public.pem` 并写入 `cosigner-public.pem`。默认 URL 为 `http://host.docker.internal:8089/cosigner/approve`。

```bash
cosigner-cli callback add ./node-callback-server/keys
```

**远程服务器：**

```bash
cosigner-cli callback add https://risk.example.com/cosigner/approve \
  --server-public-key ./server-public.pem
```

CLI 会打印 `cosigner-public.pem` 的路径和指纹；请将其安装到你的服务器上。如使用私有 CA，请添加 `--ca ./ca.pem`。

`callback add` 会更新 `config.yaml`，设置 `require_callback: true` 并重启 Cosigner（使用 `--no-restart` 可跳过重启）。`cosigner-cli info` 会显示当前的 URL 和指纹。

```yaml
approval:
  require_callback: true
callback:
  url: https://risk.example.com/cosigner/approve
  timeout: 30s
  retry_count: 1
  private_key:
    type: file
    config:
      path: /run/secrets/cosigner-private.der.hex
  response_public_key_path: /run/secrets/server-public.pem
  # ca_cert_path: /run/secrets/callback-ca.pem
```

- `approval.require_callback` (boolean): 没有回调时拒绝所有会话。生产环境中请保持为 `true`。
- `callback.url` (string): 仅支持 HTTPS。`host.docker.internal` 允许使用普通 HTTP。如果未指定路径，会自动追加 `/cosigner/approve`。
- `callback.timeout` (duration): 包括重试在内的总时长预算，同时也是请求 JWT 的过期时间。默认为 `30s`。
- `callback.retry_count` (integer): 发生网络错误或 `5xx` 时重试，使用同一个令牌。最大值和默认值均为 `1`。
- `callback.private_key` (secret provider): Cosigner 的 RSA-4096 签名密钥，十六进制编码的 PKCS#8 DER。
- `callback.response_public_key_path` (string): 你的服务器的 RSA-4096 公钥（PEM）。
- `callback.ca_cert_path` (string): 用于私有或自签名服务器证书的 CA 证书。

## 请求

JWT 头部为 `{"alg": "RS256", "typ": "JWT"}`。只接受 `RS256`，并在读取 claims 之前使用 `cosigner-public.pem` 进行验证。

- `request_id` (string (UUID)): 每个请求唯一，同时也是 `jti`。请在响应中原样返回，并将其用作幂等键。
- `iat, exp` (number): `exp` = `iat` + 超时时间。
- `cosigner_id, workspace_id, participant_id` (string): Cosigner 身份。
- `session_id, orchestrator_id` (string): 会话以及启动该会话的 Orchestrator。
- `coordinator_sequence, coordinator_signature` (number, string): 已经过验证的 Orchestrator 签名。请记录下来以便追溯。
- `protocol` (string): `ECDSA` 或 `EdDSA`。
- `operation` (string): `KEYGEN`、`SIGN`、`RESHARE` 或 `PING`。
- `threshold, participants` (number, array): 每个参与方的 `participant_id`、`party_key`、`identity_public_key`。
- `key_id` (string): 钱包密钥。
- `signing_input` (string (base64)): 仅用于 `SIGN`。待签名的确切字节。
- `signing_input_sha256` (string (hex)): 仅用于 `SIGN`。`signing_input` 的 SHA-256 哈希。
- `signing_context` (object): 仅用于 `SIGN`，可选。来自 Fystack 的交易详情，最大 64 KiB。
- `derivation` (object): 仅用于 `SIGN`，可选。ECDSA 非硬化派生的 `path` 和 `delta`。
- `new_threshold, new_participants` (number, array): 仅用于 `RESHARE`。

```json
{
  "request_id": "3f1c2a8e-7b0d-4d0c-9a51-2f6c4f0e9b12",
  "operation": "SIGN",
  "protocol": "ECDSA",
  "key_id": "8b0f3c4e-...",
  "signing_input": "q83vEjRWeJA...",
  "signing_input_sha256": "5d41402abc4b2a76...",
  "signing_context": {
    "version": 1,
    "apex_request_id": "wd_01J9...",
    "reference": { "type": "onchain_tx", "id": "..." },
    "requested_at": "2026-10-12T08:30:00Z",
    "chain": { "network": "ethereum", "chain_id": "1", "protocol": "ECDSA" },
    "wallet": { "wallet_id": "8b0f3c4e-...", "derivation_path": [44, 60, 0, 0, 0] },
    "tx": {
      "raw": "0x02f8...",
      "payload_hash": "0x5d41402a...",
      "verified": true,
      "verification_mode": "evm_transaction",
      "decoded": { "to_address": "0xAbC...", "value": "1000000000000000000" }
    }
  },
  "threshold": 1,
  "participants": [ ... ]
}
```

`verification_mode`：`evm_transaction`、`solana_message`、`tron_transaction`、`bitcoin_transaction` 或 `digest_only`。

> **Note:** `signing_context` 只是提示，不是证明。Cosigner 不会将其与 `signing_input` 进行比对。请自行解码 `tx.raw`，重新计算哈希并与 `signing_input` 比较，然后检查目标地址、金额和风险。无法验证的请求一律拒绝。

## 响应

HTTP `200`，`Content-Type: application/jwt`，使用你的服务器密钥进行 RS256 签名。最大 64 KiB。不会跟随重定向。

- `request_id` (string): 必须与请求一致。
- `action` (string): `approve` 或 `reject`。对于 `PING`，返回 `pong`。
- `reason` (string): 使用 `reject` 时必填，最大 1024 字节；使用 `approve` 时省略。会写入审计日志。
- `exp` (number, required): 请保持较短，例如 60 秒。

```json
{
  "request_id": "3f1c2a8e-7b0d-4d0c-9a51-2f6c4f0e9b12",
  "action": "reject",
  "reason": "destination_not_allowlisted",
  "iat": 1791794400,
  "exp": 1791794460,
  "jti": "b7e5..."
}
```

## 存活检测

Cosigner 会在启动时以及之后每 30 秒发送一次 `"operation": "PING"`。请直接回复 `"action": "pong"`，无需执行策略。

- 启动时 ping 失败：Cosigner 会停止运行。
- 之后的 ping 失败：健康状态显示 `callback_unavailable`，在恢复之前所有签名都会被拒绝。

## 示例服务器

`cosigner-cli` 在 `examples/node-callback-server` 中附带了一个 Node.js 示例（`express` + `jose`）。

```js
app.post('/cosigner/approve', express.text({ type: 'application/jwt' }), async (req, res) => {
  try {
    // RS256 only, signed by the pinned cosigner key, with exp and request_id.
    const request = await verifier.verify(req.body.trim())

    if (request.operation === 'PING') {
      return res.type('application/jwt').send(await signer.sign(request, { action: 'pong' }))
    }

    let decision = { action: 'approve' }
    if (request.operation === 'SIGN' && !(await policyAllows(request))) {
      decision = { action: 'reject', reason: 'destination_not_allowlisted' }
    }

    res.type('application/jwt').send(await signer.sign(request, decision))
  } catch (err) {
    res.sendStatus(401)
  }
})
```

> **Note:** 该示例默认会批准所有请求。在使用真实资金之前，请替换其中的决策逻辑。

## 生产环境检查清单

- 保持 `require_callback: true`。`cosigner-cli callback remove` 会完全禁用审批。
- 默认拒绝。
- 解码 `tx.raw` 并与 `signing_input` 进行匹配。
- 执行允许列表、限额和 AML 检查。
- 基于 `request_id` 保证幂等；重试会重新发送同一个令牌。
- 将服务器密钥保存在 KMS 或 HSM 中。AWS KMS 的 `RSA_4096` 搭配 `RSASSA_PKCS1_V1_5_SHA_256` 可用于 RS256。
- 确保在超时时间内及时响应。
- 针对 `callback_unavailable` 和拒绝情况设置告警。
