Skip to content

Proposal: use ECDH key exchange to encrypt tss-lib message to preserve message confidentiality #66

Description

@anhthii

ECDH Interface Extension

First, extend the identity store interface to support ECDH operations alongside the existing Ed25519 functionality:

// pkg/identity/store.go
type ECDHStore interface {
    GenerateECDHKeyPair() (*ecdh.PrivateKey, *ecdh.PublicKey, error)
    PerformECDH(privateKey *ecdh.PrivateKey, peerPublicKey *ecdh.PublicKey) ([]byte, error)
    DeriveSymmetricKey(sharedSecret []byte, info []byte) ([]byte, error)
}

type Store interface {
    // Existing methods...
    SignMessage(message *types.TssMessage) ([]byte, error)
    VerifyInitiatorMessage(message *types.GenerateKeyMessage) error
    
    // New ECDH methods
    ECDHStore
}

ECDH Key Exchange Session

Create a dedicated ECDH key exchange session that follows the same pattern as the existing MPC sessions 1 :

// pkg/mpc/ecdh_session.go
package mpc

import (
    "crypto/ecdh"
    "crypto/rand"
    "fmt"
    "time"
    
    "github.com/fystack/mpcium/pkg/identity"
    "github.com/fystack/mpcium/pkg/messaging"
    "github.com/fystack/mpcium/pkg/types"
)

type ECDHSession struct {
    nodeID           string
    peerIDs          []string
    pubSub           messaging.PubSub
    direct           messaging.DirectMessaging
    identityStore    identity.Store
    symmetricKeys    map[string][]byte // peerID -> symmetric key
    privateKey       *ecdh.PrivateKey
    publicKey        *ecdh.PublicKey
    exchangeComplete chan struct{}
    errCh            chan error
}

type ECDHMessage struct {
    From      string    `json:"from"`
    To        string    `json:"to"`
    PublicKey []byte    `json:"public_key"`
    Timestamp time.Time `json:"timestamp"`
    Signature []byte    `json:"signature"`
}

func NewECDHSession(
    nodeID string,
    peerIDs []string,
    pubSub messaging.PubSub,
    direct messaging.DirectMessaging,
    identityStore identity.Store,
) *ECDHSession {
    return &ECDHSession{
        nodeID:           nodeID,
        peerIDs:          peerIDs,
        pubSub:           pubSub,
        direct:           direct,
        identityStore:    identityStore,
        symmetricKeys:    make(map[string][]byte),
        exchangeComplete: make(chan struct{}),
        errCh:            make(chan error),
    }
}

func (e *ECDHSession) StartKeyExchange() error {
    // Generate ECDH key pair
    privateKey, err := ecdh.P256().GenerateKey(rand.Reader)
    if err != nil {
        return fmt.Errorf("failed to generate ECDH key pair: %w", err)
    }
    
    e.privateKey = privateKey
    e.publicKey = privateKey.PublicKey()
    
    // Subscribe to ECDH messages
    sub, err := e.pubSub.Subscribe(fmt.Sprintf("ecdh:exchange:%s", e.nodeID))
    if err != nil {
        return fmt.Errorf("failed to subscribe to ECDH topic: %w", err)
    }
    
    go e.handleIncomingMessages(sub)
    
    // Broadcast public key to all peers
    return e.broadcastPublicKey()
}

func (e *ECDHSession) broadcastPublicKey() error {
    publicKeyBytes := e.publicKey.Bytes()
    
    for _, peerID := range e.peerIDs {
        msg := ECDHMessage{
            From:      e.nodeID,
            To:        peerID,
            PublicKey: publicKeyBytes,
            Timestamp: time.Now(),
        }
        
        // Sign the message using existing identity store
        msgBytes, _ := json.Marshal(msg)
        signature, err := e.identityStore.SignMessage(&types.TssMessage{
            WalletID: "ecdh-exchange",
            Data:     msgBytes,
        })
        if err != nil {
            return fmt.Errorf("failed to sign ECDH message: %w", err)
        }
        
        msg.Signature = signature
        signedMsgBytes, _ := json.Marshal(msg)
        
        topic := fmt.Sprintf("ecdh:exchange:%s", peerID)
        if err := e.direct.Send(topic, signedMsgBytes); err != nil {
            return fmt.Errorf("failed to send ECDH message to %s: %w", peerID, err)
        }
    }
    
    return nil
}

func (e *ECDHSession) handleIncomingMessages(sub messaging.Subscription) {
    for {
        msg, err := sub.NextMessage()
        if err != nil {
            e.errCh <- err
            return
        }
        
        var ecdhMsg ECDHMessage
        if err := json.Unmarshal(msg.Data, &ecdhMsg); err != nil {
            continue
        }
        
        // Verify message signature
        if !e.verifyECDHMessage(&ecdhMsg) {
            continue
        }
        
        // Perform ECDH key exchange
        if err := e.processECDHMessage(&ecdhMsg); err != nil {
            e.errCh <- err
            return
        }
        
        // Check if exchange is complete
        if len(e.symmetricKeys) == len(e.peerIDs) {
            close(e.exchangeComplete)
            return
        }
    }
}

func (e *ECDHSession) processECDHMessage(msg *ECDHMessage) error {
    peerPublicKey, err := ecdh.P256().NewPublicKey(msg.PublicKey)
    if err != nil {
        return fmt.Errorf("invalid peer public key: %w", err)
    }
    
    // Perform ECDH
    sharedSecret, err := e.privateKey.ECDH(peerPublicKey)
    if err != nil {
        return fmt.Errorf("ECDH failed: %w", err)
    }
    
    // Derive symmetric key using HKDF
    symmetricKey := e.deriveSymmetricKey(sharedSecret, msg.From)
    e.symmetricKeys[msg.From] = symmetricKey
    
    return nil
}

func (e *ECDHSession) GetSymmetricKey(peerID string) ([]byte, bool) {
    key, exists := e.symmetricKeys[peerID]
    return key, exists
}

func (e *ECDHSession) WaitForCompletion() error {
    select {
    case <-e.exchangeComplete:
        return nil
    case err := <-e.errCh:
        return err
    case <-time.After(30 * time.Second):
        return fmt.Errorf("ECDH key exchange timeout")
    }
}

Integration with Node Initialization

Integrate the ECDH session into the node startup process in cmd/mpcium/main.go 2 :

// cmd/mpcium/main.go - after mpcNode creation
mpcNode := mpc.NewNode(
    nodeID,
    peerNodeIDs,
    pubsub,
    directMessaging,
    badgerKV,
    keyinfoStore,
    peerRegistry,
    identityStore,
)

// Perform ECDH key exchange before starting event consumer
ecdhSession := mpc.NewECDHSession(nodeID, peerNodeIDs, pubsub, directMessaging, identityStore)
if err := ecdhSession.StartKeyExchange(); err != nil {
    logger.Fatal("Failed to start ECDH key exchange", err)
}

if err := ecdhSession.WaitForCompletion(); err != nil {
    logger.Fatal("ECDH key exchange failed", err)
}

logger.Info("ECDH key exchange completed successfully")

Enhanced Message Encryption

Modify the session's message handling to use symmetric encryption 3 :

// pkg/mpc/session.go - enhanced handleTssMessage
func (s *session) handleTssMessage(keyshare tss.Message) {
    data, routing, err := keyshare.WireBytes()
    if err != nil {
        s.ErrCh <- err
        return
    }

    tssMsg := types.NewTssMessage(s.walletID, data, routing.IsBroadcast, routing.From, routing.To)
    
    // Encrypt message data if symmetric keys are available
    if s.ecdhSession != nil && !routing.IsBroadcast {
        for _, to := range routing.To {
            nodeID := PartyIDToRoutingDest(to)
            if symmetricKey, exists := s.ecdhSession.GetSymmetricKey(nodeID); exists {
                encryptedData, err := s.encryptWithSymmetricKey(data, symmetricKey)
                if err != nil {
                    s.ErrCh <- fmt.Errorf("failed to encrypt message: %w", err)
                    return
                }
                tssMsg.Data = encryptedData
                tssMsg.Encrypted = true
            }
        }
    }
    
    // Sign the message as before
    signature, err := s.identityStore.SignMessage(&tssMsg)
    if err != nil {
        s.ErrCh <- fmt.Errorf("failed to sign message: %w", err)
        return
    }
    tssMsg.Signature = signature
    
    // Continue with existing message sending logic...
}

Notes

This implementation provides:

  1. ECDH key exchange during node startup before MPC operations begin
  2. Symmetric encryption for direct messages between nodes using derived keys
  3. Integration with the existing Ed25519 authentication system for message signing
  4. Compatibility with the current session architecture 4

The ECDH session follows the same patterns as existing MPC sessions and integrates cleanly with the current messaging infrastructure. The symmetric keys derived from ECDH can be used to encrypt TSS protocol messages for confidentiality while maintaining the existing authentication guarantees.

Wiki pages you might want to explore:

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions