How MPC Nodes Implement Message Encryption Using Ed25519 and X25519 ECDH in mpcium
In mpcium, MPC nodes establish per-peer symmetric keys via X25519 Elliptic Curve Diffie-Hellman (ECDH) and encrypt point-to-point TSS messages using AES-GCM, with Ed25519 providing the underlying identity and signature verification infrastructure.
The fystack/mpcium repository implements a hybrid encryption scheme that secures multi-party computation (MPC) communication. While Ed25519 handles identity verification and message signing, the actual message encryption between MPC nodes relies on X25519 ECDH to generate shared secrets, which are then used as symmetric keys for AES-GCM encryption of direct TSS messages.
X25519 ECDH Key Exchange and Symmetric Key Derivation
When a node receives an ECDHMessage in pkg/mpc/key_exchange_session.go (lines 24-27), it first verifies the Ed25519 signature to authenticate the sender. The node then performs an X25519 ECDH operation between its private key and the peer's public key to generate a shared secret.
The implementation derives a 32-byte symmetric key from this shared secret using HKDF. The deriveSymmetricKey function produces the encryption key, which is immediately stored via SetSymmetricKey for future use.
// pkg/mpc/key_exchange_session.go lines 24-27
sharedSecret, _ := e.privateKey.ECDH(peerPublicKey) // X25519 shared secret
symmetricKey := e.deriveSymmetricKey(sharedSecret, ecdhMsg.From)
e.identityStore.SetSymmetricKey(ecdhMsg.From, symmetricKey) // store per‑peer key
Storing Per-Peer Encryption Keys
The SetSymmetricKey method in pkg/identity/identity.go (line 415) persists the 32-byte symmetric key in the identity store, indexed by the peer's node ID. This creates a secure lookup table mapping each verified peer to its unique encryption key, enabling efficient key retrieval during subsequent message exchanges.
Encrypting Direct TSS Messages
For point-to-point communication, the session handler in pkg/mpc/session.go (lines 58-70) marshals TSS messages and invokes identityStore.EncryptMessage. This wrapper method retrieves the stored symmetric key for the target peer and applies AES-GCM encryption via the encryption package.
The process produces a ciphertext that includes the nonce prepended to the encrypted payload, eliminating the need for separate nonce transmission.
// pkg/mpc/session.go lines 58-70 (simplified flow)
msg, _ := types.MarshalTssMessage(&tssMsg) // serialize
cipher, err := s.identityStore.EncryptMessage(msg, toNodeID) // AES‑GCM
s.direct.SendToOther(topic, cipher) // send encrypted payload
AES-GCM Implementation with Nonce Embedding
The actual cryptographic operations are implemented in pkg/encryption/aes.go (lines 40-86). The EncryptAESGCMWithNonceEmbed function generates a cryptographically secure random nonce, seals the plaintext using AES-GCM, and returns the concatenation nonce‖ciphertext.
// pkg/encryption/aes.go lines 40-61 (EncryptAESGCMWithNonceEmbed)
func EncryptAESGCMWithNonceEmbed(plaintext, key []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
// … create GCM, generate nonce …
ciphertext := aead.Seal(nil, nonce, plaintext, nil)
return append(nonce, ciphertext...), nil // nonce prefixed
}
Decryption uses DecryptAESGCMWithNonceEmbed, which extracts the prepended nonce and verifies the authentication tag before returning the plaintext, ensuring both confidentiality and integrity.
Decrypting Incoming Messages
Upon receiving an encrypted payload, the node calls DecryptMessage from pkg/identity/identity.go (lines 503-514). This method retrieves the sender-specific symmetric key and invokes the AES-GCM decryption routine. After successful authentication and decryption, the plaintext is unmarshaled into a TSS message structure for processing.
// Decryption flow in session handling
plaintext, err := s.identityStore.DecryptMessage(cipher, senderID) // AES‑GCM
msg, err := types.UnmarshalTssMessage(plaintext) // deserialize
s.receiveTssMessage(msg) // process
Summary
- X25519 ECDH in
pkg/mpc/key_exchange_session.goestablishes per-peer symmetric keys, providing forward secrecy for message encryption between MPC nodes. - HKDF derivation converts X25519 shared secrets into 32-byte AES keys stored via
SetSymmetricKeyinpkg/identity/identity.go. - AES-GCM encryption in
pkg/encryption/aes.goprovides authenticated encryption with nonce embedding viaEncryptAESGCMWithNonceEmbed. - Ed25519 signatures verify peer identity during the key exchange phase, preventing man-in-the-middle attacks before symmetric encryption begins.
- Direct peer-to-point TSS messages receive full AES-GCM encryption, while broadcast messages remain signed but unencrypted to allow NAT server inspection.
Frequently Asked Questions
What is the difference between Ed25519 and X25519 in mpcium's encryption scheme?
Ed25519 provides the cryptographic identity layer and digital signatures used to verify ECDHMessage authenticity. X25519 performs the Elliptic Curve Diffie-Hellman key exchange that generates the actual shared secrets used for symmetric encryption. Together, they form the "Ed2SSI9" flow: Ed25519 for identity, X25519 for key establishment.
Why does mpcium use AES-GCM instead of encrypting directly with Ed25519?
AES-GCM is used because symmetric encryption is orders of magnitude faster than asymmetric operations and produces smaller ciphertexts. After the initial X25519 ECDH handshake establishes the symmetric key, AES-GCM efficiently handles the high-volume TSS message traffic between MPC nodes without the computational overhead of asymmetric encryption.
How does mpcium distinguish between broadcast and direct message encryption?
Broadcast messages are signed with Ed25519 but transmitted in plaintext, allowing intermediate NAT servers to inspect routing metadata. Only direct point-to-point TSS messages are encrypted using the per-peer AES-GCM keys established via X25519 ECDH, ensuring payload confidentiality between specific nodes.
Where are the symmetric encryption keys stored in mpcium?
The 32-byte symmetric keys derived from X25519 ECDH are stored in the identity store via SetSymmetricKey in pkg/identity/identity.go (line 415). These keys are indexed by peer node ID and persist for the duration of the session, enabling rapid lookup during EncryptMessage and DecryptMessage operations.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →