Implémentation d'un Portefeuille MPC en Go

Un portefeuille basé sur le calcul multipartite (MPC) est un schéma de gestion décentralisée des clés privées. Il fragmente la clé privée en plusieurs morceaux qui sont ensuite distribués et stockés par différentes parties. Par rapport aux modèles traditionnels de stockage centralisé, cette approche améliore considérablement la sécurité et la résilience du système.

Principes fondamentaux de l'MPC

  • Absence de point unique de défaillance : La clé privée complète n'existe jamais sur une seule machine.
  • Signature à seuil : La collaboration d'un nombre minimum de parties est requise pour générer une signature valide.
  • Confidentialité de la clé : Aucune partie individuelle ne peut visualiser la clé privée intégrale.
  • Capacité de récupération : Le système peut se rétablir même si certains fragments sont perdus.
  • Dynamicité des participants : Les parties impliquées peuvent être ajoutées ou retirées de manière dynamique.

Conception de l'Architecture

Sélection des Dépendances

module portefeuille-mpc

go 1.21

require (
    // Bibliothèques cryptographiques
    github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0
    github.com/cloudflare/circl v1.3.3
    github.com/gtank/merlin v0.1.1
    golang.org/x/crypto v0.14.0

    // Implémentations MPC
    github.com/bnb-chain/tss-lib v0.1.0
    github.com/taurusgroup/multi-party-sig v0.0.0-20231025100644-2d4e2a6b5c6a

    // Communication réseau
    github.com/libp2p/go-libp2p v0.30.0
    github.com/libp2p/go-libp2p-core v0.20.0
    github.com/gorilla/websocket v1.5.0

    // Stockage et gestion
    github.com/google/tink/go v1.7.0
    github.com/syndtr/goleveldb v1.0.1

    // Adaptateurs blockchain
    github.com/ethereum/go-ethereum v1.12.0
    github.com/btcsuite/btcd v0.23.5

    // Outils
    github.com/spf13/viper v1.16.0
    github.com/gin-gonic/gin v1.9.1
    github.com/sirupsen/logrus v1.9.3
    github.com/stretchr/testify v1.8.4
)

Implémentation des Modules Principaux

1. Gestionnaire de Fragments de Clé

// pkg/mpc/gestionnaire_fragments.go
package mpc

import (
    "crypto/rand"
    "encoding/json"
    "errors"
    "fmt"
    "math/big"
    "time"

    "github.com/decred/dcrd/dcrec/secp256k1/v4"
)

// Fragment de clé privée
type FragmentCle struct {
    Identifiant   string        `json:"identifiant"`
    IdPartie      int           `json:"id_partie"`
    Seuil         int           `json:"seuil"`
    NbTotalParties int          `json:"nb_total_parties"`
    ValeurX       *big.Int      `json:"valeur_x"` // Coordonnée x
    ValeurY       *big.Int      `json:"valeur_y"` // Secret partagé évalué
    Publique      []byte        `json:"publique"` // Partie de clé publique
    Infos         InfosFragment `json:"infos"`
}

// Métadonnées du fragment
type InfosFragment struct {
    Version   string `json:"version"`
    Courbe    string `json:"courbe"`
    CreeLe    int64  `json:"cree_le"`
    ModifieLe int64  `json:"modifie_le"`
    Chiffre   bool   `json:"chiffre"`
    IdCle     string `json:"id_cle"`
}

// Service de gestion des fragments
type ServiceFragments struct {
    depot      Depot
    crypto     ServiceCrypto
    parametres Config
}

func NouveauServiceFragments(depot Depot, crypto ServiceCrypto, parametres Config) *ServiceFragments {
    return &ServiceFragments{
        depot:      depot,
        crypto:     crypto,
        parametres: parametres,
    }
}

// Créer une répartition de Shamir
func (s *ServiceFragments) CreerRepartitionShamir(
    secret *big.Int,
    seuil, total int,
) ([]*FragmentCle, error) {

    if seuil > total {
        return nil, errors.New("le seuil ne peut pas excéder le nombre total de fragments")
    }

    // Générer les coefficients du polynôme aléatoire
    coeffs := make([]*big.Int, seuil)
    coeffs[0] = secret
    ordre := secp256k1.S256().N

    for i := 1; i < seuil; i++ {
        c, err := rand.Int(rand.Reader, ordre)
        if err != nil {
            return nil, err
        }
        coeffs[i] = c
    }

    // Calculer les points (fragments) du polynôme
    fragments := make([]*FragmentCle, total)
    for i := 1; i <= total; i++ {
        pointX := big.NewInt(int64(i))
        pointY := s.evaluerPolynome(coeffs, pointX, ordre)

        clePubPartielle := s.calculerClePubliquePartielle(pointY)

        fragments[i-1] = &FragmentCle{
            Identifiant:    fmt.Sprintf("frag-%d-%d", i, time.Now().UnixNano()),
            IdPartie:       i,
            Seuil:          seuil,
            NbTotalParties: total,
            ValeurX:        pointX,
            ValeurY:        pointY,
            Publique:       clePubPartielle,
            Infos: InfosFragment{
                Version: "1.0",
                Courbe:  "secp256k1",
                CreeLe:  time.Now().Unix(),
                Chiffre: true,
            },
        }
    }

    return fragments, nil
}

// Évaluation du polynôme de partage
func (s *ServiceFragments) evaluerPolynome(coeffs []*big.Int, x, modulo *big.Int) *big.Int {
    resultat := new(big.Int)
    for puissance, coeff := range coeffs {
        terme := new(big.Int).Exp(x, big.NewInt(int64(puissance)), modulo)
        terme.Mul(terme, coeff)
        terme.Mod(terme, modulo)
        resultat.Add(resultat, terme)
        resultat.Mod(resultat, modulo)
    }
    return resultat
}

// Générer la composante publique à partir du fragment secret
func (s *ServiceFragments) calculerClePubliquePartielle(fragmentSecret *big.Int) []byte {
    courbe := secp256k1.S256()
    px, py := courbe.ScalarBaseMult(fragmentSecret.Bytes())
    return secp256k1.NewPublicKey(px, py).SerializeCompressed()
}

// Reconstruire le secret initial
func (s *ServiceFragments) ReconstruireSecret(fragments []*FragmentCle) (*big.Int, error) {
    seuilRequis := fragments[0].Seuil
    if len(fragments) < seuilRequis {
        return nil, errors.New("nombre de fragments insuffisant pour la reconstruction")
    }

    // Utilisation de l'interpolation de Lagrange
    secretReconstitue := new(big.Int)
    modulo := secp256k1.S256().N
    fragmentsSelectionnes := fragments[:seuilRequis]

    for i, fi := range fragmentsSelectionnes {
        // Calcul du coefficient de Lagrange Li(0)
        numerateur := new(big.Int).SetInt64(1)
        denominateur := new(big.Int).SetInt64(1)

        for j, fj := range fragmentsSelectionnes {
            if i == j {
                continue
            }
            numerateur.Mul(numerateur, new(big.Int).Neg(fj.ValeurX))
            denominateur.Mul(denominateur, new(big.Int).Sub(fi.ValeurX, fj.ValeurX))
        }

        denominateur.ModInverse(denominateur, modulo)
        coeffLagrange := new(big.Int).Mul(numerateur, denominateur)
        coeffLagrange.Mod(coeffLagrange, modulo)

        // Contribution de ce fragment: yi * Li(0)
        contribution := new(big.Int).Mul(fi.ValeurY, coeffLagrange)
        contribution.Mod(contribution, modulo)

        secretReconstitue.Add(secretReconstitue, contribution)
        secretReconstitue.Mod(secretReconstitue, modulo)
    }

    return secretReconstitue, nil
}

2. Coordinateur pour la Signature à Seuil (TSS)

// pkg/tss/coordinateur.go
package tss

import (
    "context"
    "fmt"
    "sync"

    "github.com/bnb-chain/tss-lib/ecdsa/keygen"
    "github.com/bnb-chain/tss-lib/ecdsa/signing"
    "github.com/bnb-chain/tss-lib/tss"
)

// Configuration d'un participant
type ConfigParticipant struct {
    Identite   *tss.PartyID
    Seuil      int
    Total      int
    Tous       []*tss.PartyID
    CanalMsg   chan tss.Message
    CanalFin   chan *tss.SignatureData
    CanalErr   chan error
}

// Orchestrateur du protocole TSS
type OrchestrateurTSS struct {
    mutex            sync.RWMutex
    participants     map[string]*ConfigParticipant
    contexte         *tss.PeerContext
    preParametres    *keygen.LocalPreParams
    donneesGenCle    *keygen.LocalPartySaveData
    idSession        string
    delaiExpiration  time.Duration
}

func NouvelOrchestrateurTSS(total, seuil int) (*OrchestrateurTSS, error) {
    if seuil > total {
        return nil, fmt.Errorf("le seuil %d dépasse le total %d", seuil, total)
    }

    params, err := keygen.GeneratePreParams(10 * time.Minute)
    if err != nil {
        return nil, err
    }

    return &OrchestrateurTSS{
        participants:   make(map[string]*ConfigParticipant),
        preParametres:  params,
        delaiExpiration: 5 * time.Minute,
    }, nil
}

// Exécution de la Génération Distribuée de Clés (DKG)
func (o *OrchestrateurTSS) GenererCleDist(ctx context.Context) (*ResultatDKG, error) {
    o.mutex.Lock()
    defer o.mutex.Unlock()

    ids := make([]*tss.PartyID, 0, len(o.participants))
    for _, p := range o.participants {
        ids = append(ids, p.Identite)
    }
    o.contexte = tss.NewPeerContext(ids)

    var wg sync.WaitGroup
    resultats := make([]*keygen.LocalPartySaveData, len(ids))
    canalErreurs := make(chan error, len(ids))

    for idx, id := range ids {
        wg.Add(1)
        go func(index int, pid *tss.PartyID) {
            defer wg.Done()
            // Logique de lancement de la partie locale pour le DKG
            // ... (code de communication réseau simplifié)
            select {
            case data := <-o.participants[pid.Id].CanalFin:
                resultats[index] = data.(*keygen.LocalPartySaveData)
            case err := <-o.participants[pid.Id].CanalErr:
                canalErreurs <- err
            case <-ctx.Done():
                canalErreurs <- ctx.Err()
            case <-time.After(o.delaiExpiration):
                canalErreurs <- fmt.Errorf("délai dépassé pour la génération de clé")
            }
        }(idx, id)
    }

    wg.Wait()
    close(canalErreurs)

    for err := range canalErreurs {
        if err != nil {
            return nil, err
        }
    }

    // Vérification de cohérence entre les participants
    o.donneesGenCle = resultats[0]
    clePub := o.donneesGenCle.ECDSAPub

    for i := 1; i < len(resultats); i++ {
        if resultats[i].ECDSAPub.X().Cmp(clePub.X()) != 0 ||
           resultats[i].ECDSAPub.Y().Cmp(clePub.Y()) != 0 {
            return nil, fmt.Errorf("incohérence des clés générées entre les parties")
        }
    }

    return &ResultatDKG{
        ClePublique: &ecdsa.PublicKey{
            Curve: secp256k1.S256(),
            X:     clePub.X(),
            Y:     clePub.Y(),
        },
        Donnees:  o.donneesGenCle,
        Parties:  ids,
    }, nil
}

// Exécution d'une signature à seuil
func (o *OrchestrateurTSS) SignerMessage(ctx context.Context, message []byte, signataires []*tss.PartyID) (*SignatureFinale, error) {
    seuilRequis := o.participants[signataires[0].Id].Seuil
    if len(signataires) < seuilRequis {
        return nil, fmt.Errorf("nombre de signataires (%d) inférieur au seuil (%d)", len(signataires), seuilRequis)
    }

    // Logique de coordination de la signature
    // ... (code similaire à la DKG, mais avec le protocole de signature)

    // Récupération et vérification des parts de signature
    var signaturePartielle *tss.SignatureData
    // ... (logique de collecte)

    return &SignatureFinale{
        R: signaturePartielle.R,
        S: signaturePartielle.S,
        V: signaturePartielle.SignatureRecovery,
    }, nil
}

type ResultatDKG struct {
    ClePublique *ecdsa.PublicKey
    Donnees     *keygen.LocalPartySaveData
    Parties     []*tss.PartyID
}

type SignatureFinale struct {
    R *big.Int
    S *big.Int
    V byte
}

3. Couche Réseau Peer-to-Peer

// pkg/reseau/noeud_p2p.go
package reseau

import (
    "context"
    "encoding/json"
    "sync"
    "time"

    "github.com/libp2p/go-libp2p"
    "github.com/libp2p/go-libp2p-core/host"
    "github.com/libp2p/go-libp2p-core/peer"
    "github.com/libp2p/go-libp2p-core/protocol"
    "github.com/multiformats/go-multiaddr"
)

// Configuration du nœud
type ConfigNoeud struct {
    Ecoute       string   `yaml:"ecoute"`
    PairAmorce   []string `yaml:"pair_amorce"`
    MaxPairs     int      `yaml:"max_pairs"`
    MinPairs     int      `yaml:"min_pairs"`
    IdProtocole  string   `yaml:"id_protocole"`
}

// Représentation d'un pair connu
type PairConnecte struct {
    Identifiant peer.ID
    Adresse     multiaddr.Multiaddr
    Actif       bool
    VuDernier   time.Time
}

// Nœud du réseau P2P
type NoeudP2P struct {
    hote          host.Host
    configuration ConfigNoeud
    pairsActifs   map[peer.ID]*PairConnecte
    mutex         sync.RWMutex
    abonnements   map[string]TraiteurMessage
}

type MessageReseau struct {
    Type      string      `json:"type"`
    IdSession string      `json:"id_session"`
    Emetteur  string      `json:"emetteur"`
    Charge    interface{} `json:"charge"`
}

type TraiteurMessage func(msg *MessageReseau) error

func NouveauNoeudP2P(config ConfigNoeud) (*NoeudP2P, error) {
    optionsHote := []libp2p.Option{
        libp2p.ListenAddrStrings(config.Ecoute),
        libp2p.NATPortMap(),
        libp2p.EnableAutoRelay(),
    }

    h, err := libp2p.New(optionsHote...)
    if err != nil {
        return nil, err
    }

    noeud := &NoeudP2P{
        hote:          h,
        configuration: config,
        pairsActifs:   make(map[peer.ID]*PairConnecte),
        abonnements:   make(map[string]TraiteurMessage),
    }

    h.SetStreamHandler(protocol.ID(config.IdProtocole), noeud.gererFluxEntrant)

    go noeud.maintenirConnexions()

    return noeud, nil
}

// Établir une connexion avec un pair
func (n *NoeudP2P) SeConnecter(ctx context.Context, adresse string) error {
    maddr, err := multiaddr.NewMultiaddr(adresse)
    if err != nil {
        return err
    }

    info, err := peer.AddrInfoFromP2pAddr(maddr)
    if err != nil {
        return err
    }

    err = n.hote.Connect(ctx, *info)
    if err != nil {
        return err
    }

    n.mutex.Lock()
    n.pairsActifs[info.ID] = &PairConnecte{
        Identifiant: info.ID,
        Adresse:     maddr,
        Actif:       true,
        VuDernier:   time.Now(),
    }
    n.mutex.Unlock()

    return nil
}

// Gestionnaire pour les flux entrants
func (n *NoeudP2P) gererFluxEntrant(s network.Stream) {
    defer s.Close()

    var msg MessageReseau
    if err := json.NewDecoder(s).Decode(&msg); err != nil {
        return
    }

    n.mutex.RLock()
    traiteur, ok := n.abonnements[msg.Type]
    n.mutex.RUnlock()

    if ok {
        if err := traiteur(&msg); err != nil {
            // Gestion d'erreur
        }
    }
}

// Diffusion d'un message à tous les pairs connectés
func (n *NoeudP2P) DiffuserMessage(ctx context.Context, msg *MessageReseau) error {
    n.mutex.RLock()
    destinataires := make([]peer.ID, 0)
    for pid, info := range n.pairsActifs {
        if info.Actif {
            destinataires = append(destinataires, pid)
        }
    }
    n.mutex.RUnlock()

    var wg sync.WaitGroup
    for _, pid := range destinataires {
        wg.Add(1)
        go func(dest peer.ID) {
            defer wg.Done()
            // Ouvrir un flux et envoyer le message
        }(pid)
    }
    wg.Wait()

    return nil
}

// Inscription d'un traitement pour un type de message
func (n *NoeudP2P) SAbonner(typeMsg string, traiteur TraiteurMessage) {
    n.mutex.Lock()
    defer n.mutex.Unlock()
    n.abonnements[typeMsg] = traiteur
}

4. Module de Stockage Sécurisé

// pkg/securite/stockage_safegarde.go
package securite

import (
    "crypto/aes"
    "crypto/cipher"
    "crypto/rand"
    "encoding/base64"
    "os"
    "path/filepath"

    "golang.org/x/crypto/argon2"
)

// Configuration du stockage
type ConfigStockage struct {
    Repertoire string `json:"repertoire"`
    FichierCle string `json:"fichier_cle"`
    Algorithme string `json:"algorithme"`
}

// Service de stockage chiffré
type StockageSafegarde struct {
    config    ConfigStockage
    cleMaitre []byte
}

func NouveauStockageSafegarde(config ConfigStockage) (*StockageSafegarde, error) {
    s := &StockageSafegarde{config: config}
    if err := s.initialiserCleMaitre(); err != nil {
        return nil, err
    }
    return s, nil
}

// Initialisation de la clé maître
func (s *StockageSafegarde) initialiserCleMaitre() error {
    cheminCle := filepath.Join(s.config.Repertoire, s.config.FichierCle)

    if _, err := os.Stat(cheminCle); os.IsNotExist(err) {
        // Générer une nouvelle clé et la protéger avec un mot de passe
        cleBrute := make([]byte, 32)
        if _, err := rand.Read(cleBrute); err != nil {
            return err
        }

        motDePasse := s.demanderMotDePasse()
        cleChiffree := s.chiffrerAvecMDP(cleBrute, motDePasse)

        if err := os.WriteFile(cheminCle, cleChiffree, 0600); err != nil {
            return err
        }
        s.cleMaitre = cleBrute
    } else {
        // Charger la clé existante
        cleChiffree, err := os.ReadFile(cheminCle)
        if err != nil {
            return err
        }
        motDePasse := s.demanderMotDePasse()
        cle, err := s.dechiffrerAvecMDP(cleChiffree, motDePasse)
        if err != nil {
            return err
        }
        s.cleMaitre = cle
    }
    return nil
}

// Chiffrement des données avec la clé maître
func (s *StockageSafegarde) Chiffrer(donnees []byte, idCle string) ([]byte, error) {
    cleDerivee := s.deriverCle(idCle)

    bloc, err := aes.NewCipher(cleDerivee)
    if err != nil {
        return nil, err
    }

    gcm, err := cipher.NewGCM(bloc)
    if err != nil {
        return nil, err
    }

    nonce := make([]byte, gcm.NonceSize())
    if _, err := rand.Read(nonce); err != nil {
        return nil, err
    }

    resultat := gcm.Seal(nonce, nonce, donnees, nil)
    return resultat, nil
}

// Déchiffrement des données
func (s *StockageSafegarde) Dechiffrer(donnees []byte, idCle string) ([]byte, error) {
    cleDerivee := s.deriverCle(idCle)

    bloc, err := aes.NewCipher(cleDerivee)
    if err != nil {
        return nil, err
    }

    gcm, err := cipher.NewGCM(bloc)
    if err != nil {
        return nil, err
    }

    tailleNonce := gcm.NonceSize()
    nonce, texteChiffre := donnees[:tailleNonce], donnees[tailleNonce:]

    return gcm.Open(nil, nonce, texteChiffre, nil)
}

// Dérivation d'une clé de chiffrement spécifique
func (s *StockageSafegarde) deriverCle(id string) []byte {
    sel := []byte(id) // Simplifié, utiliser un hachage plus robuste en production
    return argon2.IDKey(s.cleMaitre, sel, 1, 64*1024, 4, 32)
}

// Suppression sécurisée d'une clé dérivée
func (s *StockageSafegarde) SupprimerCleSecurise(id string) error {
    // Implémenter la suppression sûre des données associées
    return nil
}

Considérations de Sécurité

  1. Stockage des fragments :
    • Chaque fragment doit être chiffré individuellement.
    • L'utilisation d'un module de sécurité matériel (HSM) est recommandée pour un niveau de protection renforcé.
    • Des sauvegardes régulières et des tests de restauration sont indispensables.
  2. Sécurité des communications :
    • Chiffrement des échanges (TLS, Noise).
    • Authentification et intégrité des messages.
    • Mesures contre les attaques par rejeu.
  3. Sécurité du protocole :
    • Utilisation de protocoles MPC standardisés et éprouvés (ex: GG20).
    • Mécanismes de détection des participants malveillants.
    • Audits de sécurité réguliers du code.
  4. Sécurité opérationnelle :
    • Validation explicite des opérations sensibles par l'utilisateur.
    • Journaux d'audit complets et immuables.
    • Authentification multifacteur pour l'accès.

Étiquettes: Go MPC Portefeuille cryptographie TSS

Publié le 21 juin à 20h49