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

工作原理
- Cosigner 验证 Orchestrator 发来的
session_start。 - Cosigner 向你的 URL
POST一个 RS256 JWT(Content-Type: application/jwt)。 - 你的服务器验证该 JWT,执行策略,然后返回
200以及一个 RS256 JWT。 - Cosigner 验证响应和
request_id,然后加入会话。
KEYGEN、SIGN 和 RESHARE 都会发送请求。请对 SIGN 应用交易策略。
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。
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。
signing_context 只是提示,不是证明。Cosigner 不会将其与 signing_input 进行比对。请自行解码 tx.raw,重新计算哈希并与 signing_input 比较,然后检查目标地址、金额和风险。无法验证的请求一律拒绝。
响应
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和拒绝情况设置告警。