审批回调

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

Cosigner 向回调发送已签名的请求,回调返回 approve 或 reject
Click to open the full-size diagram

工作原理

  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 应用交易策略。

配置

需要两对 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。

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

远程服务器:

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 和指纹。

config.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
  • Name
    approval.require_callback
    Type
    boolean
    Description

    没有回调时拒绝所有会话。生产环境中请保持为 true。

  • Name
    callback.url
    Type
    string
    Description

    仅支持 HTTPS。host.docker.internal 允许使用普通 HTTP。如果未指定路径,会自动追加 /cosigner/approve。

  • Name
    callback.timeout
    Type
    duration
    Description

    包括重试在内的总时长预算,同时也是请求 JWT 的过期时间。默认为 30s。

  • Name
    callback.retry_count
    Type
    integer
    Description

    发生网络错误或 5xx 时重试,使用同一个令牌。最大值和默认值均为 1。

  • Name
    callback.private_key
    Type
    secret provider
    Description

    Cosigner 的 RSA-4096 签名密钥,十六进制编码的 PKCS#8 DER。

  • Name
    callback.response_public_key_path
    Type
    string
    Description

    你的服务器的 RSA-4096 公钥(PEM)。

  • Name
    callback.ca_cert_path
    Type
    string
    Description

    用于私有或自签名服务器证书的 CA 证书。

请求

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

  • Name
    request_id
    Type
    string (UUID)
    Description

    每个请求唯一,同时也是 jti。请在响应中原样返回,并将其用作幂等键。

  • Name
    iat, exp
    Type
    number
    Description

    exp = iat + 超时时间。

  • Name
    cosigner_id, workspace_id, participant_id
    Type
    string
    Description

    Cosigner 身份。

  • Name
    session_id, orchestrator_id
    Type
    string
    Description

    会话以及启动该会话的 Orchestrator。

  • Name
    coordinator_sequence, coordinator_signature
    Type
    number, string
    Description

    已经过验证的 Orchestrator 签名。请记录下来以便追溯。

  • Name
    protocol
    Type
    string
    Description

    ECDSA 或 EdDSA。

  • Name
    operation
    Type
    string
    Description

    KEYGEN、SIGN、RESHARE 或 PING。

  • Name
    threshold, participants
    Type
    number, array
    Description

    每个参与方的 participant_id、party_key、identity_public_key。

  • Name
    key_id
    Type
    string
    Description

    钱包密钥。

  • Name
    signing_input
    Type
    string (base64)
    Description

    仅用于 SIGN。待签名的确切字节。

  • Name
    signing_input_sha256
    Type
    string (hex)
    Description

    仅用于 SIGN。signing_input 的 SHA-256 哈希。

  • Name
    signing_context
    Type
    object
    Description

    仅用于 SIGN,可选。来自 Fystack 的交易详情,最大 64 KiB。

  • Name
    derivation
    Type
    object
    Description

    仅用于 SIGN,可选。ECDSA 非硬化派生的 path 和 delta。

  • Name
    new_threshold, new_participants
    Type
    number, array
    Description

    仅用于 RESHARE。

Decoded SIGN request

{
  "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。

响应

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

  • Name
    request_id
    Type
    string
    Description

    必须与请求一致。

  • Name
    action
    Type
    string
    Description

    approve 或 reject。对于 PING,返回 pong。

  • Name
    reason
    Type
    string
    Description

    使用 reject 时必填,最大 1024 字节;使用 approve 时省略。会写入审计日志。

  • Name
    exp
    Type
    number
    Required
    required
    Description

    请保持较短,例如 60 秒。

Response payload

200
{
  "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)。

server.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)
  }
})

生产环境检查清单

  • 保持 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 和拒绝情况设置告警。