Files
fhe/pkg/keycommit/commit.go
T
zeekayandHanzo Dev 41d57d3c1e Threshold FHE moves to luxfi/threshold/protocols/tfhe
pkg/keycommit keeps what stays: a Shamir split, over the RFC 3526 Group-14
prime, of the SHA-256 commitment to a key. Its shares cannot decrypt, so
CommitKey and KeyCommitment replace GenerateSharedKey and KeyShare. fhed's
threshold mode follows the rename.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-17 13:26:44 -07:00

299 lines
8.0 KiB
Go

// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package keycommit
import (
"bytes"
"crypto/sha256"
"encoding/gob"
"fmt"
"math/big"
"github.com/luxfi/fhe"
)
// KeyCommitment represents a party's share of the FHE secret key
type KeyCommitment struct {
PartyIndex int // 1-based party index
SessionID string // Keygen session identifier
// LSSS shares of the key components
SKLWEShare *Share // Share of the LWE secret key seed
SKBRShare *Share // Share of the BR secret key seed
// Public information (same for all parties)
Threshold int
Total int
PublicKey *fhe.PublicKey
Params fhe.Parameters
}
// CommitResult is the result of distributed key generation
type CommitResult struct {
SessionID string
Threshold int
Total int
PublicKey *fhe.PublicKey
BootstrapKey *fhe.BootstrapKey
Params fhe.Parameters
Shares []*KeyCommitment // One per party
}
// CommitKey generates a keypair in one process and Shamir-splits the SHA-256
// hash of each secret-key component. The resulting shares are shares of a hash
// and cannot perform threshold decryption.
//
// Deprecated: for threshold custody use
// github.com/luxfi/threshold/protocols/tfhe.
func CommitKey(params fhe.Parameters, threshold, total int, sessionID string) (*CommitResult, error) {
if threshold > total {
return nil, fmt.Errorf("threshold %d exceeds total %d", threshold, total)
}
// Generate regular FHE keys first
keygen := fhe.NewKeyGenerator(params)
sk, pk := keygen.GenKeyPair()
bsk := keygen.GenBootstrapKey(sk)
// Derive secrets from the key components for sharing
// We hash the serialized keys to get field elements
skLWEBytes, err := serializeRLWESecretKey(sk.SKLWE)
if err != nil {
return nil, fmt.Errorf("failed to serialize SKLWE: %w", err)
}
skLWESeed := hashToFieldElement(skLWEBytes)
skBRBytes, err := serializeRLWESecretKey(sk.SKBR)
if err != nil {
return nil, fmt.Errorf("failed to serialize SKBR: %w", err)
}
skBRSeed := hashToFieldElement(skBRBytes)
// Split secrets using LSSS
lweSharings, err := SplitSecret(skLWESeed, threshold, total)
if err != nil {
return nil, fmt.Errorf("failed to split SKLWE: %w", err)
}
brSharings, err := SplitSecret(skBRSeed, threshold, total)
if err != nil {
return nil, fmt.Errorf("failed to split SKBR: %w", err)
}
// Create key shares for each party
shares := make([]*KeyCommitment, total)
for i := 0; i < total; i++ {
shares[i] = &KeyCommitment{
PartyIndex: i + 1,
SessionID: sessionID,
SKLWEShare: lweSharings.Shares[i],
SKBRShare: brSharings.Shares[i],
Threshold: threshold,
Total: total,
PublicKey: pk,
Params: params,
}
}
return &CommitResult{
SessionID: sessionID,
Threshold: threshold,
Total: total,
PublicKey: pk,
BootstrapKey: bsk,
Params: params,
Shares: shares,
}, nil
}
// ReshareCommitment performs LSSS resharing to change threshold or add/remove parties
func ReshareCommitment(oldShares []*KeyCommitment, newThreshold, newTotal int, newSessionID string) (*CommitResult, error) {
if len(oldShares) == 0 {
return nil, fmt.Errorf("no shares provided")
}
oldThreshold := oldShares[0].Threshold
if len(oldShares) < oldThreshold {
return nil, fmt.Errorf("not enough shares: have %d, need %d", len(oldShares), oldThreshold)
}
// Extract LSSS shares
lweShares := make([]*Share, len(oldShares))
brShares := make([]*Share, len(oldShares))
for i, ks := range oldShares {
lweShares[i] = ks.SKLWEShare
brShares[i] = ks.SKBRShare
}
// Reshare to new parameters
newLWEShares, err := Reshare(lweShares, oldThreshold, newThreshold, newTotal)
if err != nil {
return nil, fmt.Errorf("failed to reshare SKLWE: %w", err)
}
newBRShares, err := Reshare(brShares, oldThreshold, newThreshold, newTotal)
if err != nil {
return nil, fmt.Errorf("failed to reshare SKBR: %w", err)
}
// Create new key shares (public key remains the same)
newShares := make([]*KeyCommitment, newTotal)
for i := 0; i < newTotal; i++ {
newShares[i] = &KeyCommitment{
PartyIndex: i + 1,
SessionID: newSessionID,
SKLWEShare: newLWEShares.Shares[i],
SKBRShare: newBRShares.Shares[i],
Threshold: newThreshold,
Total: newTotal,
PublicKey: oldShares[0].PublicKey, // Same public key
Params: oldShares[0].Params,
}
}
return &CommitResult{
SessionID: newSessionID,
Threshold: newThreshold,
Total: newTotal,
PublicKey: oldShares[0].PublicKey,
BootstrapKey: nil, // Would need to regenerate or share
Params: oldShares[0].Params,
Shares: newShares,
}, nil
}
// AddParty adds a new party by computing their share
func AddParty(existingShares []*KeyCommitment, newPartyIndex int, newSessionID string) (*KeyCommitment, error) {
if len(existingShares) == 0 {
return nil, fmt.Errorf("no shares provided")
}
threshold := existingShares[0].Threshold
if len(existingShares) < threshold {
return nil, fmt.Errorf("not enough shares: have %d, need %d", len(existingShares), threshold)
}
// Extract LSSS shares
lweShares := make([]*Share, len(existingShares))
brShares := make([]*Share, len(existingShares))
for i, ks := range existingShares {
lweShares[i] = ks.SKLWEShare
brShares[i] = ks.SKBRShare
}
// Compute new share
newLWEShare, err := AddShare(lweShares, threshold, newPartyIndex)
if err != nil {
return nil, fmt.Errorf("failed to add SKLWE share: %w", err)
}
newBRShare, err := AddShare(brShares, threshold, newPartyIndex)
if err != nil {
return nil, fmt.Errorf("failed to add SKBR share: %w", err)
}
return &KeyCommitment{
PartyIndex: newPartyIndex,
SessionID: newSessionID,
SKLWEShare: newLWEShare,
SKBRShare: newBRShare,
Threshold: threshold,
Total: existingShares[0].Total + 1,
PublicKey: existingShares[0].PublicKey,
Params: existingShares[0].Params,
}, nil
}
// RefreshCommitments refreshes all shares to provide proactive security
func RefreshCommitments(shares []*KeyCommitment, newSessionID string) ([]*KeyCommitment, error) {
if len(shares) == 0 {
return nil, fmt.Errorf("no shares provided")
}
threshold := shares[0].Threshold
total := len(shares)
// Collect current shares
lweSet := &ShareSet{
Threshold: threshold,
Total: total,
Shares: make([]*Share, total),
}
brSet := &ShareSet{
Threshold: threshold,
Total: total,
Shares: make([]*Share, total),
}
for i, ks := range shares {
lweSet.Shares[i] = ks.SKLWEShare
brSet.Shares[i] = ks.SKBRShare
}
// Refresh shares
newLWESet, err := RefreshShares(lweSet)
if err != nil {
return nil, fmt.Errorf("failed to refresh SKLWE: %w", err)
}
newBRSet, err := RefreshShares(brSet)
if err != nil {
return nil, fmt.Errorf("failed to refresh SKBR: %w", err)
}
// Create new key shares
newShares := make([]*KeyCommitment, total)
for i := 0; i < total; i++ {
newShares[i] = &KeyCommitment{
PartyIndex: shares[i].PartyIndex,
SessionID: newSessionID,
SKLWEShare: newLWESet.Shares[i],
SKBRShare: newBRSet.Shares[i],
Threshold: threshold,
Total: total,
PublicKey: shares[0].PublicKey,
Params: shares[0].Params,
}
}
return newShares, nil
}
// Helpers
func hashToFieldElement(data []byte) *big.Int {
hash := sha256.Sum256(data)
n := new(big.Int).SetBytes(hash[:])
return n.Mod(n, Prime)
}
func serializeRLWESecretKey(sk interface{}) ([]byte, error) {
var buf bytes.Buffer
enc := gob.NewEncoder(&buf)
if err := enc.Encode(sk); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// MarshalCommitment serializes a key share
func MarshalCommitment(ks *KeyCommitment) ([]byte, error) {
var buf bytes.Buffer
enc := gob.NewEncoder(&buf)
if err := enc.Encode(ks); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// UnmarshalCommitment deserializes a key share
func UnmarshalCommitment(data []byte) (*KeyCommitment, error) {
dec := gob.NewDecoder(bytes.NewReader(data))
var ks KeyCommitment
if err := dec.Decode(&ks); err != nil {
return nil, err
}
return &ks, nil
}