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é
- 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.
- Sécurité des communications :
- Chiffrement des échanges (TLS, Noise).
- Authentification et intégrité des messages.
- Mesures contre les attaques par rejeu.
- 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.
- 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.