Callback de aprobación
Antes de unirse a cualquier sesión, el cosigner envía una solicitud firmada a tu servidor y espera un approve firmado. Tu servidor recibe los bytes exactos que se van a firmar, por lo que puede verificar la transacción de forma independiente de Fystack.

Cómo funciona
- El cosigner verifica el
session_startdel orquestador. - El cosigner hace
POSTde un JWT RS256 a tu URL (Content-Type: application/jwt). - Tu servidor lo verifica, ejecuta su política y devuelve
200con un JWT RS256. - El cosigner verifica la respuesta y el
request_id, y luego se une a la sesión.
Se envían solicitudes para KEYGEN, SIGN y RESHARE. Aplica la política de transacciones a SIGN.
El cosigner falla de forma cerrada. Un rechazo, una respuesta distinta de 200, una redirección, un timeout, un cuerpo demasiado grande, una firma incorrecta o un request_id equivocado significan que no hay firma. La sesión falla con CALLBACK_REJECTED.
Configuración
Dos pares de claves RSA-4096. Solo se intercambian las claves públicas.
- Clave del cosigner: la crea
cosigner-cliy firma las solicitudes. Entregacosigner-public.pema tu servidor. - Clave del servidor: se crea en tu servidor o en KMS y firma las respuestas. Entrega
server-public.pemal cosigner.
Mismo host: apunta a la carpeta de claves del servidor. La CLI lee server-public.pem y escribe cosigner-public.pem. La URL predeterminada es http://host.docker.internal:8089/cosigner/approve.
cosigner-cli callback add ./node-callback-server/keys
Servidor remoto:
cosigner-cli callback add https://risk.example.com/cosigner/approve \
--server-public-key ./server-public.pem
La CLI muestra la ruta y la huella digital de cosigner-public.pem; instálalo en tu servidor. Agrega --ca ./ca.pem si usas una CA privada.
callback add actualiza config.yaml, establece require_callback: true y reinicia el cosigner (omítelo con --no-restart). cosigner-cli info muestra la URL y la huella digital actuales.
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
Rechaza todas las sesiones si no hay callback. Mantenlo en
trueen producción.
- Name
- callback.url
- Type
- string
- Description
Solo HTTPS. Se permite HTTP sin cifrar para
host.docker.internal. Si no hay ruta, se agrega/cosigner/approve.
- Name
- callback.timeout
- Type
- duration
- Description
Tiempo total disponible, incluido el reintento. También es la expiración del JWT de la solicitud. Predeterminado:
30s.
- Name
- callback.retry_count
- Type
- integer
- Description
Reintentos ante errores de red o
5xx, con el mismo token. Máximo y predeterminado:1.
- Name
- callback.private_key
- Type
- secret provider
- Description
Clave de firma RSA-4096 del cosigner, en hex PKCS#8 DER.
- Name
- callback.response_public_key_path
- Type
- string
- Description
Clave pública RSA-4096 de tu servidor (PEM).
- Name
- callback.ca_cert_path
- Type
- string
- Description
Certificado de CA para un certificado de servidor privado o autofirmado.
Solicitud
Header JWT {"alg": "RS256", "typ": "JWT"}. Acepta solo RS256 y verifica contra cosigner-public.pem antes de leer los claims.
- Name
- request_id
- Type
- string (UUID)
- Description
Único por solicitud, también es el
jti. Devuélvelo en la respuesta. Úsalo como clave de idempotencia.
- Name
- iat, exp
- Type
- number
- Description
exp=iat+ timeout.
- Name
- cosigner_id, workspace_id, participant_id
- Type
- string
- Description
Identidad del cosigner.
- Name
- session_id, orchestrator_id
- Type
- string
- Description
La sesión y el orquestador que la inició.
- Name
- coordinator_sequence, coordinator_signature
- Type
- number, string
- Description
La firma del orquestador ya verificada. Regístrala para la trazabilidad.
- Name
- protocol
- Type
- string
- Description
ECDSAoEdDSA.
- Name
- operation
- Type
- string
- Description
KEYGEN,SIGN,RESHAREoPING.
- Name
- threshold, participants
- Type
- number, array
- Description
El
participant_id,party_keyeidentity_public_keyde cada participante.
- Name
- key_id
- Type
- string
- Description
Clave de la billetera.
- Name
- signing_input
- Type
- string (base64)
- Description
Solo
SIGN. Bytes exactos que se van a firmar.
- Name
- signing_input_sha256
- Type
- string (hex)
- Description
Solo
SIGN. SHA-256 designing_input.
- Name
- signing_context
- Type
- object
- Description
Solo
SIGN, opcional. Detalles de la transacción proporcionados por Fystack, máximo 64 KiB.
- Name
- derivation
- Type
- object
- Description
Solo
SIGN, opcional.pathydeltaECDSA no endurecidos.
- Name
- new_threshold, new_participants
- Type
- number, array
- Description
Solo
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 o digest_only.
signing_context es una pista, no una prueba. El cosigner no lo contrasta con signing_input. Decodifica tx.raw por tu cuenta, recalcula el hash, compáralo con signing_input y luego revisa el destino, el monto y el riesgo. Rechaza lo que no puedas verificar.
Respuesta
HTTP 200, Content-Type: application/jwt, RS256 con la clave de tu servidor. Máximo 64 KiB. No se siguen las redirecciones.
- Name
- request_id
- Type
- string
- Description
Debe coincidir con la solicitud.
- Name
- action
- Type
- string
- Description
approveoreject.pongparaPING.
- Name
- reason
- Type
- string
- Description
Obligatorio con
reject, máximo 1024 bytes, se omite conapprove. Se escribe en el registro de auditoría.
- Name
- exp
- Type
- number
- Required
- required
- Description
Mantenlo corto, p. ej., 60s.
Response payload
200{
"request_id": "3f1c2a8e-7b0d-4d0c-9a51-2f6c4f0e9b12",
"action": "reject",
"reason": "destination_not_allowlisted",
"iat": 1791794400,
"exp": 1791794460,
"jti": "b7e5..."
}
Liveness
El cosigner envía "operation": "PING" al iniciar y cada 30 segundos. Responde "action": "pong" sin ejecutar la política.
- Si falla el ping de inicio: el cosigner se detiene.
- Si falla un ping posterior: el estado de salud muestra
callback_unavailabley la firma se rechaza hasta que se recupere.
Servidor de ejemplo
cosigner-cli incluye un ejemplo en Node.js en examples/node-callback-server (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)
}
})
Tal como se distribuye, el ejemplo aprueba todo. Reemplaza la lógica de decisión antes de usar fondos reales.
Checklist para producción
- Mantén
require_callback: true.cosigner-cli callback removedesactiva la aprobación por completo. - Deniega por defecto.
- Decodifica
tx.rawy compáralo consigning_input. - Aplica listas de permitidos, límites y controles AML.
- Sé idempotente según el
request_id; los reintentos reenvían el mismo token. - Guarda la clave del servidor en KMS o en un HSM. AWS KMS
RSA_4096conRSASSA_PKCS1_V1_5_SHA_256funciona para RS256. - Responde con holgura dentro del timeout.
- Configura alertas para
callback_unavailabley para los rechazos.