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.

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
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
// 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
// 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
// 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 |
|---|---|---|
| Crear la sesión de firma con los detalles de la transacción | Verificar que el ID de sesión sea único |
| Seleccionar el número de partes del umbral que participarán | Verificar que las partes estén en línea y autenticadas |
| Intercambiar compromisos sobre las claves efímeras | Validar el formato y la vigencia de los compromisos |
| Cada parte calcula y comparte su firma parcial | Verificar la validez de la firma parcial |
| Combinar las firmas parciales en la firma final | Verificar la firma final contra la clave pública |
| 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
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
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
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
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:
- Cada nodo genera un par de claves X25519 efímero y se suscribe al topic compartido
ecdh:exchange. - Los nodos difunden su clave pública junto con un timestamp, firmados con su identidad Ed25519 (
SignEcdhMessage). - Los receptores verifican la firma, ejecutan
ECDH()con su propia clave privada y derivan una clave simétrica con HKDF-SHA256. - La clave derivada de 32 bytes se almacena en caché por peer mediante
identityStore.SetSymmetricKeyy la reutilizan los helpers AES-GCM anteriores.
Derivación de claves HKDF
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
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