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.

El cosigner envía una solicitud firmada al callback, que devuelve approve o reject
Click to open the full-size diagram

Cómo funciona

  1. El cosigner verifica el session_start del orquestador.
  2. El cosigner hace POST de un JWT RS256 a tu URL (Content-Type: application/jwt).
  3. Tu servidor lo verifica, ejecuta su política y devuelve 200 con un JWT RS256.
  4. 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.

Configuración

Dos pares de claves RSA-4096. Solo se intercambian las claves públicas.

  • Clave del cosigner: la crea cosigner-cli y firma las solicitudes. Entrega cosigner-public.pem a tu servidor.
  • Clave del servidor: se crea en tu servidor o en KMS y firma las respuestas. Entrega server-public.pem al 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 true en 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

    ECDSA o EdDSA.

  • Name
    operation
    Type
    string
    Description

    KEYGEN, SIGN, RESHARE o PING.

  • Name
    threshold, participants
    Type
    number, array
    Description

    El participant_id, party_key e identity_public_key de 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 de signing_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. path y delta ECDSA 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.

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

    approve o reject. pong para PING.

  • Name
    reason
    Type
    string
    Description

    Obligatorio con reject, máximo 1024 bytes, se omite con approve. 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_unavailable y 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)
  }
})

Checklist para producción

  • Mantén require_callback: true. cosigner-cli callback remove desactiva la aprobación por completo.
  • Deniega por defecto.
  • Decodifica tx.raw y compáralo con signing_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_4096 con RSASSA_PKCS1_V1_5_SHA_256 funciona para RS256.
  • Responde con holgura dentro del timeout.
  • Configura alertas para callback_unavailable y para los rechazos.