Seguridad de billeteras MPC

Fystack usa Multi-Party Computation (MPC) para eliminar los puntos únicos de falla en la gestión de claves privadas. Ninguna parte posee nunca una clave privada completa. En su lugar, los fragmentos de clave se distribuyen entre múltiples nodos y las firmas se calculan de forma colaborativa mediante criptografía de umbral.

Este documento describe cómo la implementación MPC de Fystack protege las claves de las billeteras mediante confianza distribuida, comunicación segura y computación verificable.

Arquitectura de seguridad de billeteras MPC de Fystack
Click to open the full-size diagram

Responsabilidades de los nodos

Cada nodo:

  • Almacena localmente solo su fragmento cifrado.
  • Participa en la firma de umbral mediante verificación Ed25519 y comunicación P2P protegida con ECDH.
  • No puede reconstruir ni exportar la clave completa, incluso si se ve comprometido.

Propiedad

Descripción

Confidencialidad

Ningún nodo u operador puede acceder por sí solo a una clave privada completa.

Integridad

Todos los mensajes entre nodos se autentican mediante firmas Ed25519 o de Cloud KMS.

Resiliencia

Admite n nodos con umbral t, tolerando hasta (t-1) fallas de nodos.

Cifrado en reposo

Los fragmentos de clave se almacenan localmente con cifrado AES.

Cifrado en tránsito

Se aplica mediante NATS TLS más una capa ECDH para los canales P2P.


1. Descripción general

Multi-Party Computation permite que múltiples partes calculen conjuntamente operaciones criptográficas sin que ninguna de ellas tenga acceso a claves privadas completas. Fystack implementa esquemas de firma de umbral en los que un número mínimo de partes (el umbral) debe cooperar para producir una firma válida.

Componente

Descripción

Nodos MPC

Un clúster se forma con un mínimo de 3 nodos.

Umbral (t)

La billetera sigue siendo segura aunque hasta (t-1) nodos estén comprometidos.

Fragmentos de clave

La clave privada se divide matemáticamente en múltiples fragmentos.

Cifrado de fragmentos

Cada fragmento se cifra con AES (cifrado predeterminado de BadgerDB).

Seguridad de mensajería

Protección de doble capa: NATS TLS más intercambio de claves ECDH para el cifrado P2P.

Autenticación de mensajes

Apex se comunica con el clúster MPC mediante mensajes firmados (Ed25519 o Cloud KMS).

Beneficios clave

  • Sin punto único de compromiso: comprometer un nodo no expone las claves privadas
  • Confianza distribuida: múltiples partes independientes deben cooperar
  • Transparente para la blockchain: las firmas resultantes son firmas ECDSA/EdDSA estándar
  • Umbral dinámico: permite ajustar los requisitos de firma según el valor de la transacción
  • Registro de auditoría: cada sesión de firma queda registrada y es trazable

2. Generación distribuida de claves (DKG)

Fystack usa Distributed Key Generation para crear las claves de las billeteras sin que ninguna parte posea nunca la clave privada completa.

Proceso DKG

Fase 1: Compromiso

  • Cada parte genera un polinomio aleatorio de grado t-1 (donde t es el umbral)
  • Las partes difunden compromisos sobre sus polinomios
  • Los compromisos se verifican mediante el VSS (Verifiable Secret Sharing) de Feldman

Fase 2: Distribución de fragmentos

  • Cada parte evalúa su polinomio en puntos específicos
  • Los fragmentos se envían de forma segura a las partes correspondientes a través de canales cifrados
  • Cada parte recibe un fragmento de cada una de las demás partes

Fase 3: Verificación

  • Las partes verifican los fragmentos recibidos contra los compromisos difundidos
  • Se identifican los fragmentos inválidos y se registran reclamos
  • El protocolo se aborta si se detectan demasiados fragmentos inválidos

Fase 4: Construcción de la clave

  • Cada parte combina los fragmentos válidos para formar su fragmento de clave
  • La clave pública se calcula a partir de los compromisos
  • Ninguna parte conoce la clave privada completa

Configuración de DKG

yaml
mpc_threshold: 2   # Number of MPC nodes required to sign

mpc_threshold representa el umbral t en un esquema n-of-t. Con n nodos MPC activos, el sistema exige 2 ≤ t ≤ n, tolerando hasta (t - 1) nodos defectuosos o desconectados sin dejar de producir firmas. Por ejemplo, n = 3 con mpc_threshold: 2 da como resultado un clúster 2-of-3: cualquier par de nodos autenticados puede firmar, pero comprometer un solo nodo no basta para recuperar la clave privada.

Para protocolos MPC robustos del estilo GG18/GG20, además recomendamos t > n / 2, lo que garantiza tolerancia a fallas bizantinas: incluso si f = n - t nodos son maliciosos, están desconectados o actúan en colusión, los nodos honestos siguen siendo mayoría y pueden impedir firmas falsas o la reconstrucción de la clave privada.

Propiedades de seguridad

  • Robustez: tolera hasta n-t partes maliciosas
  • Verificabilidad: todas las partes pueden verificar la corrección de los fragmentos
  • Privacidad: no se filtra información sobre la clave privada
  • No interactivo: tras la configuración, no se requiere más DKG

3. Firma de umbral

La firma de umbral permite que un subconjunto de partes (que alcance el umbral) produzca de forma colaborativa una firma válida sin reconstruir la clave privada.

Protocolo de firma (ECDSA)

Ronda 1: fase de compromiso

Generación de compromisos

pseudo
// Each signer generates commitment
const k_i = randomScalar()
const gamma_i = randomScalar()
const commitment_i = {
  K_i: k_i * G,           // Ephemeral public key
  Gamma_i: gamma_i * G,   // Blinding factor
}
broadcastCommitment(commitment_i)

Ronda 2: generación de firmas parciales

Firma parcial

pseudo
// After receiving all commitments
const R = combineCommitments(allCommitments)
const r = R.x  // x-coordinate of combined point

// Each party computes partial signature
const s_i = k_i + r * keyShare_i * hash(message)
broadcastPartialSignature(s_i)

Ronda 3: agregación de firmas

Combinación de firmas

pseudo
// Combine partial signatures
const s = lagrangeInterpolation(partialSignatures)
const signature = { r, s }

// Verify signature
const valid = ecdsaVerify(signature, publicKey, message)

Flujo de firma

Paso

Acción

Control de seguridad

  1. Inicio de sesión

Crear la sesión de firma con los detalles de la transacción

Verificar que el ID de sesión sea único

  1. Selección de partes

Seleccionar el número de partes del umbral que participarán

Verificar que las partes estén en línea y autenticadas

  1. Ronda de compromisos

Intercambiar compromisos sobre las claves efímeras

Validar el formato y la vigencia de los compromisos

  1. Firmas parciales

Cada parte calcula y comparte su firma parcial

Verificar la validez de la firma parcial

  1. Agregación

Combinar las firmas parciales en la firma final

Verificar la firma final contra la clave pública

  1. Difusión

Enviar la transacción firmada a la blockchain

Confirmar la aceptación de la transacción


4. Seguridad de red

Transporte NATS (TLS)

Los nodos MPC intercambian tráfico a través del bus NATS compartido declarado en config.yaml. Los despliegues en producción requieren endpoints TLS y certificados de cliente fijados:

Configuración de NATS TLS

yaml
nats:
  url: tls://nats.example.com:4222
  username: ""
  password: ""
  tls:
    client_cert: "/etc/mpcium/certs/client-cert.pem"
    client_key: "/etc/mpcium/certs/client-key.pem"
    ca_cert: "/etc/mpcium/certs/rootCA.pem"

El script deployments/systemd/setup-config.sh se niega a ejecutarse en producción sin la URL tls:// y las rutas nats.tls.* anteriores, lo que garantiza que cada nodo se comunique con NATS a través de un túnel TLS autenticado.

Bus de mensajes firmados

Antes de publicar un payload TSS, el nodo lo firma con su identidad Ed25519 (consulta pkg/identity/fileStore):

Firmar mensajes TSS

go
func (s *fileStore) SignMessage(msg *types.TssMessage) ([]byte, error) {
	msgBytes, err := msg.MarshalForSigning()
	if err != nil {
		return nil, fmt.Errorf("failed to marshal message for signing: %w", err)
	}
	signature := ed25519.Sign(s.privateKey, msgBytes)
	return signature, nil
}

Cada nodo verifica la firma antes de aceptar un mensaje, lo que impide peers suplantados o datos reenviados:

Verificar mensajes TSS

go
func (s *fileStore) VerifyMessage(msg *types.TssMessage) error {
	if msg.Signature == nil {
		return fmt.Errorf("message has no signature")
	}
	publicKey, err := s.GetPublicKey(partyIDToNodeID(msg.From))
	if err != nil {
		return fmt.Errorf("failed to get sender's public key: %w", err)
	}
	msgBytes, err := msg.MarshalForSigning()
	if err != nil {
		return fmt.Errorf("failed to marshal message for verification: %w", err)
	}
	if !ed25519.Verify(publicKey, msgBytes, msg.Signature) {
		return fmt.Errorf("invalid signature")
	}
	return nil
}

Cifrado punto a punto

Los nodos ejecutan un intercambio ECDH sobre topics NATS dedicados (pkg/mpc/key_exchange_session.go) para derivar claves simétricas por peer. Esas claves impulsan el cifrado AES-GCM de cada payload:

Cifrar payloads TSS

go
func (s *fileStore) EncryptMessage(plaintext []byte, peerID string) ([]byte, error) {
	key, err := s.GetSymmetricKey(peerID)
	if err != nil {
		return nil, err
	}
	if key == nil {
		return nil, fmt.Errorf("no symmetric key for peer %s", peerID)
	}
	return encryption.EncryptAESGCMWithNonceEmbed(plaintext, key)
}

Flujo del handshake ECDH

La sesión ECDH (pkg/mpc/key_exchange_session.go) se ejecuta sobre el bus NATS:

  1. Cada nodo genera un par de claves X25519 efímero y se suscribe al topic compartido ecdh:exchange.
  2. Los nodos difunden su clave pública junto con un timestamp, firmados con su identidad Ed25519 (SignEcdhMessage).
  3. Los receptores verifican la firma, ejecutan ECDH() con su propia clave privada y derivan una clave simétrica con HKDF-SHA256.
  4. La clave derivada de 32 bytes se almacena en caché por peer mediante identityStore.SetSymmetricKey y la reutilizan los helpers AES-GCM anteriores.

Derivación de claves HKDF

go
func (e *ecdhSession) deriveSymmetricKey(secret []byte, peerID string) []byte {
	info := deriveConsistentInfo(e.nodeID, peerID) // stable ordering of IDs
	hkdfReader := hkdf.New(sha256.New, secret, nil, info)
	key := make([]byte, 32) // AES-256 key
	if _, err := hkdfReader.Read(key); err != nil {
		e.errCh <- err
		return nil
	}
	return key
}

Si un peer se desconecta, RemovePeer descarta la clave simétrica en caché, por lo que se requiere un nuevo handshake al reconectarse, lo que mantiene efímeras las claves del canal.

Aislamiento de red

Control

Implementación

Red privada

Los nodos MPC se comunican a través de una VPC/VPN privada

Reglas de firewall

Solo los puertos MPC están abiertos, restringidos a IPs de nodos conocidos

Limitación de tasa

Tasa máxima de mensajes por nodo para prevenir DoS

Monitoreo de conexiones

Todas las conexiones se registran y se monitorean en busca de anomalías


5. Seguridad del almacenamiento

Los fragmentos de clave se conservan en BadgerDB. El servicio MPC se niega a abrir la base de datos si no se proporciona una clave de cifrado (badger_password), y esa clave alimenta el cifrado AES-256 de Badger mediante WithEncryptionKey(...) (pkg/kvstore/badger.go). Los backups reutilizan el mismo formato cifrado, por lo que tanto los archivos de la base de datos activa como los archivos de snapshot siguen siendo ilegibles sin la contraseña.

Cifrado de Badger

go
opts := badger.DefaultOptions(config.DBPath).
  WithEncryptionKey(config.EncryptionKey).
  WithCompression(options.ZSTD)

db, err := badger.Open(opts)

6. Resumen

La implementación MPC de Fystack elimina los puntos únicos de falla mediante confianza distribuida, criptografía de umbral y protocolos multiparte seguros. Al dividir las claves privadas en fragmentos y exigir una firma colaborativa, garantizamos que ningún compromiso aislado pueda provocar la pérdida de activos.

Funciones de seguridad clave:

  • Generación distribuida de claves sin un punto único de confianza
  • Firmas de umbral que requieren la cooperación de múltiples partes
  • Comunicación cifrada con TLS + payloads AES-GCM derivados de ECDH
  • Cifrado en reposo de BadgerDB con AES-256 y contraseñas proporcionadas por el operador
  • Computación verificable para detectar partes maliciosas