Files
mpc/pkg/threshold/crypto_adapter.go
zeekayandHanzo Dev 3f22f50c1b threshold: stop registering the nonce-leaking adapters at import
pkg/threshold's init() pushed CGGMP21Scheme and FROSTScheme into the
process-global crypto/threshold registry as a side effect of import. Two
concrete hazards:

 1. Shadowing. These adapters' signer keeps the secret nonce scalar k inside
    NonceState (k||R). Anything resolving GetScheme(SchemeCMP) would silently
    receive that signer; a NonceState crossing a party boundary yields the
    private key as x = (s*k - H(m))/r. Opting into that must be deliberate.

 2. Startup panic. RegisterScheme panics on a duplicate ID ([email protected].2
    threshold/registry.go:24-26), so the day crypto/threshold ships its own
    CMP/FROST scheme, merely importing this package kills the process at start.

Replace init() with an exported Register() documenting both hazards. Nothing
regresses: the daemon never reached this code (`go list -deps ./cmd/mpcd` does
not contain pkg/threshold) and the only importers, examples/, use
NewUnifiedThresholdAPI and never touch the registry.

register_test.go pins the invariant: TestImportDoesNotRegister fails if an
ambient init() comes back (verified red/green), TestRegisterIsExplicit proves
explicit opt-in still works.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-25 16:33:00 -07:00

1512 lines
40 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Package threshold provides adapters that implement crypto/threshold interfaces
// by wrapping the MPC protocol implementations (CGGMP21, FROST).
package threshold
import (
"context"
"crypto/ecdsa"
cryptorand "crypto/rand"
"crypto/sha256"
"errors"
"fmt"
"io"
"math/big"
"sync"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
cryptothreshold "github.com/luxfi/crypto/threshold"
"github.com/luxfi/mpc/pkg/protocol"
"github.com/luxfi/mpc/pkg/protocol/cggmp21"
"github.com/luxfi/mpc/pkg/protocol/frost"
)
// Register installs these adapters into the process-global crypto/threshold
// registry. It is NOT called from init() on purpose — importing a package must
// never silently change what every other package resolves SchemeCMP to.
//
// Two concrete hazards this avoids:
// 1. Shadowing. Anything calling cryptothreshold.GetScheme(SchemeCMP) would
// transparently get THESE adapters, whose signer keeps the secret nonce
// scalar k inside NonceState (k||R). NonceState is local-only by contract;
// serialize it across a party boundary and the private key falls out of
// (k,s,r) as x = (s·k − H(m))/r. Production must not opt into that by
// accident — the daemon does not: `go list -deps ./cmd/mpcd` does not
// contain this package, and only examples/ import it.
// 2. Startup panic. cryptothreshold.RegisterScheme panics on a duplicate ID
// ([email protected] threshold/registry.go:24-26). An ambient init() means the
// day crypto/threshold ships its own CMP/FROST scheme, merely importing
// this package kills the process at start.
//
// Call it explicitly, once, from a main() that has read the above.
func Register() {
cryptothreshold.RegisterScheme(&CGGMP21Scheme{})
cryptothreshold.RegisterScheme(&FROSTScheme{})
}
// =============================================================================
// CGGMP21 Scheme (Threshold ECDSA)
// =============================================================================
// CGGMP21Scheme implements threshold.Scheme for CGGMP21 (threshold ECDSA).
type CGGMP21Scheme struct {
protocol protocol.Protocol
once sync.Once
}
func (s *CGGMP21Scheme) init() {
s.once.Do(func() {
s.protocol = cggmp21.NewCGGMP21Protocol()
})
}
func (s *CGGMP21Scheme) ID() cryptothreshold.SchemeID {
return cryptothreshold.SchemeCMP
}
func (s *CGGMP21Scheme) Name() string {
return "CGGMP21 (Threshold ECDSA)"
}
func (s *CGGMP21Scheme) KeyShareSize() int {
return 256 // Approximate serialized size
}
func (s *CGGMP21Scheme) SignatureShareSize() int {
return 64 // ECDSA signature share
}
func (s *CGGMP21Scheme) SignatureSize() int {
return 65 // ECDSA signature (r, s, v)
}
func (s *CGGMP21Scheme) PublicKeySize() int {
return 65 // Uncompressed ECDSA public key
}
func (s *CGGMP21Scheme) NewDKG(config cryptothreshold.DKGConfig) (cryptothreshold.DKG, error) {
if err := config.Validate(); err != nil {
return nil, err
}
s.init()
return &cggmp21DKG{
scheme: s,
config: config,
}, nil
}
func (s *CGGMP21Scheme) NewTrustedDealer(config cryptothreshold.DealerConfig) (cryptothreshold.TrustedDealer, error) {
if err := config.Validate(); err != nil {
return nil, err
}
s.init()
return &cggmp21TrustedDealer{
scheme: s,
config: config,
}, nil
}
func (s *CGGMP21Scheme) NewSigner(share cryptothreshold.KeyShare) (cryptothreshold.Signer, error) {
if share == nil {
return nil, errors.New("nil key share")
}
s.init()
return &cggmp21Signer{
scheme: s,
share: share,
}, nil
}
func (s *CGGMP21Scheme) NewAggregator(groupKey cryptothreshold.PublicKey) (cryptothreshold.Aggregator, error) {
if groupKey == nil {
return nil, errors.New("nil group key")
}
s.init()
return &cggmp21Aggregator{
scheme: s,
groupKey: groupKey,
}, nil
}
func (s *CGGMP21Scheme) NewVerifier(groupKey cryptothreshold.PublicKey) (cryptothreshold.Verifier, error) {
if groupKey == nil {
return nil, errors.New("nil group key")
}
s.init()
return &cggmp21Verifier{
scheme: s,
groupKey: groupKey,
}, nil
}
func (s *CGGMP21Scheme) ParseKeyShare(data []byte) (cryptothreshold.KeyShare, error) {
return &cggmp21KeyShare{
scheme: s,
data: data,
}, nil
}
func (s *CGGMP21Scheme) ParsePublicKey(data []byte) (cryptothreshold.PublicKey, error) {
return &cggmp21PublicKey{
scheme: s,
data: data,
}, nil
}
func (s *CGGMP21Scheme) ParseSignatureShare(data []byte) (cryptothreshold.SignatureShare, error) {
return &cggmp21SignatureShare{
scheme: s,
data: data,
}, nil
}
func (s *CGGMP21Scheme) ParseSignature(data []byte) (cryptothreshold.Signature, error) {
return &cggmp21Signature{
scheme: s,
data: data,
}, nil
}
// CGGMP21 implementation types
type cggmp21DKG struct {
scheme *CGGMP21Scheme
config cryptothreshold.DKGConfig
party protocol.Party
round int
groupKey cryptothreshold.PublicKey
keyGenDone bool
keyGenError error
}
func (d *cggmp21DKG) Round1(ctx context.Context) (cryptothreshold.DKGMessage, error) {
partyIDs := make([]string, d.config.TotalParties)
for i := 0; i < d.config.TotalParties; i++ {
partyIDs[i] = fmt.Sprintf("party-%d", i)
}
selfID := partyIDs[d.config.PartyIndex]
party, err := d.scheme.protocol.KeyGen(selfID, partyIDs, d.config.Threshold)
if err != nil {
return nil, fmt.Errorf("failed to start keygen: %w", err)
}
d.party = party
d.round = 1
// Get first round message
select {
case msg := <-party.Messages():
return &cggmp21DKGMessage{
round: 1,
from: d.config.PartyIndex,
data: msg.GetData(),
}, nil
case err := <-party.Errors():
return nil, err
case <-ctx.Done():
return nil, ctx.Err()
}
}
func (d *cggmp21DKG) Round2(ctx context.Context, round1Messages map[int]cryptothreshold.DKGMessage) (cryptothreshold.DKGMessage, error) {
if d.party == nil {
return nil, errors.New("DKG not started")
}
// Process round 1 messages
for _, msg := range round1Messages {
if err := d.party.Update(&protocolMessage{data: msg.Bytes()}); err != nil {
return nil, err
}
}
d.round = 2
// Get round 2 message
select {
case msg := <-d.party.Messages():
return &cggmp21DKGMessage{
round: 2,
from: d.config.PartyIndex,
data: msg.GetData(),
}, nil
case err := <-d.party.Errors():
return nil, err
case <-ctx.Done():
return nil, ctx.Err()
}
}
func (d *cggmp21DKG) Round3(ctx context.Context, round2Messages map[int]cryptothreshold.DKGMessage) (cryptothreshold.KeyShare, error) {
if d.party == nil {
return nil, errors.New("DKG not started")
}
// Process round 2 messages
for _, msg := range round2Messages {
if err := d.party.Update(&protocolMessage{data: msg.Bytes()}); err != nil {
return nil, err
}
}
// Wait for completion
for !d.party.Done() {
select {
case err := <-d.party.Errors():
return nil, err
case <-ctx.Done():
return nil, ctx.Err()
default:
// Continue waiting
}
}
result, err := d.party.Result()
if err != nil {
return nil, err
}
config, ok := result.(protocol.KeyGenConfig)
if !ok {
return nil, errors.New("unexpected result type")
}
// Create key share from config
data, err := config.Serialize()
if err != nil {
return nil, err
}
pubKeyBytes := ellipticPublicKeyToBytes(config.GetPublicKey())
d.groupKey = &cggmp21PublicKey{
scheme: d.scheme,
data: pubKeyBytes,
}
return &cggmp21KeyShare{
scheme: d.scheme,
data: data,
index: d.config.PartyIndex,
threshold: d.config.Threshold,
totalParties: d.config.TotalParties,
groupKey: d.groupKey,
publicShare: ellipticPublicKeyToBytes(config.GetSharePublicKey()),
}, nil
}
func (d *cggmp21DKG) NumRounds() int {
return 5 // CGGMP21 has 5 rounds
}
func (d *cggmp21DKG) GroupKey() cryptothreshold.PublicKey {
return d.groupKey
}
type cggmp21TrustedDealer struct {
scheme *CGGMP21Scheme
config cryptothreshold.DealerConfig
}
func (d *cggmp21TrustedDealer) GenerateShares(ctx context.Context) ([]cryptothreshold.KeyShare, cryptothreshold.PublicKey, error) {
// For trusted dealer, we simulate DKG locally
// In production, this would generate Shamir shares of an ECDSA key
// Generate a master ECDSA key
rand := d.config.Rand
if rand == nil {
rand = defaultRand()
}
// Create placeholder shares
shares := make([]cryptothreshold.KeyShare, d.config.TotalParties)
for i := 0; i < d.config.TotalParties; i++ {
shareData := make([]byte, 32)
if _, err := io.ReadFull(rand, shareData); err != nil {
return nil, nil, err
}
shares[i] = &cggmp21KeyShare{
scheme: d.scheme,
data: shareData,
index: i,
threshold: d.config.Threshold,
totalParties: d.config.TotalParties,
}
}
// Create group public key placeholder
groupKeyData := make([]byte, 65)
groupKeyData[0] = 0x04 // Uncompressed point marker
if _, err := io.ReadFull(rand, groupKeyData[1:]); err != nil {
return nil, nil, err
}
groupKey := &cggmp21PublicKey{
scheme: d.scheme,
data: groupKeyData,
}
// Set group key on all shares
for i := range shares {
shares[i].(*cggmp21KeyShare).groupKey = groupKey
}
return shares, groupKey, nil
}
type cggmp21KeyShare struct {
scheme *CGGMP21Scheme
data []byte
index int
threshold int
totalParties int
groupKey cryptothreshold.PublicKey
publicShare []byte
}
func (s *cggmp21KeyShare) Index() int { return s.index }
func (s *cggmp21KeyShare) GroupKey() cryptothreshold.PublicKey { return s.groupKey }
func (s *cggmp21KeyShare) PublicShare() []byte { return s.publicShare }
func (s *cggmp21KeyShare) Threshold() int { return s.threshold }
func (s *cggmp21KeyShare) TotalParties() int { return s.totalParties }
func (s *cggmp21KeyShare) Bytes() []byte { return s.data }
func (s *cggmp21KeyShare) SchemeID() cryptothreshold.SchemeID { return cryptothreshold.SchemeCMP }
type cggmp21PublicKey struct {
scheme *CGGMP21Scheme
data []byte
}
func (k *cggmp21PublicKey) Bytes() []byte { return k.data }
func (k *cggmp21PublicKey) Equal(other cryptothreshold.PublicKey) bool {
if other == nil {
return false
}
otherBytes := other.Bytes()
if len(k.data) != len(otherBytes) {
return false
}
for i := range k.data {
if k.data[i] != otherBytes[i] {
return false
}
}
return true
}
func (k *cggmp21PublicKey) SchemeID() cryptothreshold.SchemeID { return cryptothreshold.SchemeCMP }
type cggmp21Signer struct {
scheme *CGGMP21Scheme
share cryptothreshold.KeyShare
}
func (s *cggmp21Signer) Index() int { return s.share.Index() }
func (s *cggmp21Signer) PublicShare() []byte { return s.share.PublicShare() }
func (s *cggmp21Signer) KeyShare() cryptothreshold.KeyShare { return s.share }
func (s *cggmp21Signer) NonceGen(ctx context.Context) (cryptothreshold.NonceCommitment, cryptothreshold.NonceState, error) {
// Generate a random scalar k on secp256k1
curve := secp256k1.S256()
n := curve.Params().N
k, err := cryptorand.Int(defaultRand(), n)
if err != nil {
return nil, nil, fmt.Errorf("failed to generate nonce scalar: %w", err)
}
if k.Sign() == 0 {
return nil, nil, errors.New("generated zero nonce")
}
// Compute commitment R = k * G on secp256k1
Rx, Ry := curve.ScalarBaseMult(k.Bytes())
// Encode R as 33-byte compressed point
rBytes := compressPointSecp256k1(Rx, Ry)
// Encode k as 32-byte big-endian scalar
kBytes := make([]byte, 32)
kB := k.Bytes()
copy(kBytes[32-len(kB):], kB)
// Nonce state: k (32 bytes, secret) || R_compressed (33 bytes, public)
stateData := make([]byte, 65)
copy(stateData[:32], kBytes)
copy(stateData[32:], rBytes)
commitment := &cggmp21NonceCommitment{from: s.share.Index(), data: rBytes}
state := &cggmp21NonceState{from: s.share.Index(), data: stateData}
return commitment, state, nil
}
func (s *cggmp21Signer) SignShare(ctx context.Context, message []byte, signers []int, nonce cryptothreshold.NonceState) (cryptothreshold.SignatureShare, error) {
if nonce == nil {
return nil, errors.New("nonce state is required for CGGMP21 signing")
}
nonceData := nonce.Bytes()
if len(nonceData) < 65 {
return nil, errors.New("invalid nonce state: expected at least 65 bytes (k || R)")
}
n := secp256k1.S256().Params().N
// Extract k (nonce scalar) and r = R.x mod n from nonce state
k := new(big.Int).SetBytes(nonceData[:32])
Rx, _ := decompressPointSecp256k1(nonceData[32:65])
if Rx == nil {
return nil, errors.New("failed to decompress R from nonce state")
}
r := new(big.Int).Set(Rx)
r.Mod(r, n)
// Get secret share d_i
shareBytes := s.share.Bytes()
if len(shareBytes) < 32 {
padded := make([]byte, 32)
copy(padded[32-len(shareBytes):], shareBytes)
shareBytes = padded
}
di := new(big.Int).SetBytes(shareBytes[:32])
// Compute Lagrange coefficient for this party among the signers
myIdx := s.share.Index()
lambda := lagrangeCoefficientAt0(myIdx, signers, n)
// Hash the message to 32 bytes
var m *big.Int
if len(message) == 32 {
m = new(big.Int).SetBytes(message)
} else {
h := sha256.Sum256(message)
m = new(big.Int).SetBytes(h[:])
}
m.Mod(m, n)
// Partial ECDSA signature: s_i = k^{-1} * (m + r * d_i * lambda_i) mod n
kInv := new(big.Int).ModInverse(k, n)
if kInv == nil {
return nil, errors.New("nonce k is not invertible mod n")
}
rdi := new(big.Int).Mul(r, di)
rdi.Mod(rdi, n)
rdi.Mul(rdi, lambda)
rdi.Mod(rdi, n)
si := new(big.Int).Add(m, rdi)
si.Mod(si, n)
si.Mul(kInv, si)
si.Mod(si, n)
// Encode as 64 bytes: r (32) || s_i (32)
shareData := make([]byte, 64)
rB := r.Bytes()
copy(shareData[32-len(rB):32], rB)
siB := si.Bytes()
copy(shareData[64-len(siB):64], siB)
return &cggmp21SignatureShare{
scheme: s.scheme,
index: s.share.Index(),
data: shareData,
}, nil
}
type cggmp21SignatureShare struct {
scheme *CGGMP21Scheme
index int
data []byte
}
func (s *cggmp21SignatureShare) Index() int { return s.index }
func (s *cggmp21SignatureShare) Bytes() []byte { return s.data }
func (s *cggmp21SignatureShare) SchemeID() cryptothreshold.SchemeID { return cryptothreshold.SchemeCMP }
type cggmp21Aggregator struct {
scheme *CGGMP21Scheme
groupKey cryptothreshold.PublicKey
}
func (a *cggmp21Aggregator) GroupKey() cryptothreshold.PublicKey { return a.groupKey }
func (a *cggmp21Aggregator) Aggregate(ctx context.Context, message []byte, shares []cryptothreshold.SignatureShare, commitments []cryptothreshold.NonceCommitment) (cryptothreshold.Signature, error) {
if len(shares) == 0 {
return nil, errors.New("no shares to aggregate")
}
n := secp256k1.S256().Params().N
halfN := new(big.Int).Rsh(n, 1)
// Each share is 64 bytes: r (32) || s_i (32).
// All shares must have the same r value.
firstShare := shares[0].Bytes()
if len(firstShare) < 64 {
return nil, errors.New("invalid share size: expected at least 64 bytes")
}
r := new(big.Int).SetBytes(firstShare[:32])
// Aggregate: s = sum(s_i) mod n
// Lagrange coefficients were already applied in SignShare.
sAgg := new(big.Int)
for _, share := range shares {
sb := share.Bytes()
if len(sb) < 64 {
return nil, fmt.Errorf("invalid share from party %d: too short", share.Index())
}
shareR := new(big.Int).SetBytes(sb[:32])
if shareR.Cmp(r) != 0 {
return nil, fmt.Errorf("share from party %d has mismatched r value", share.Index())
}
si := new(big.Int).SetBytes(sb[32:64])
sAgg.Add(sAgg, si)
sAgg.Mod(sAgg, n)
}
// Low-S normalization (Bitcoin/Ethereum consensus)
if sAgg.Cmp(halfN) > 0 {
sAgg.Sub(n, sAgg)
}
// Recovery byte v
var v byte
if r.Cmp(halfN) > 0 {
v = 1
}
// 65-byte ECDSA signature: r (32) || s (32) || v (1)
sigData := make([]byte, 65)
rBytes := r.Bytes()
copy(sigData[32-len(rBytes):32], rBytes)
sBytes := sAgg.Bytes()
copy(sigData[64-len(sBytes):64], sBytes)
sigData[64] = v
return &cggmp21Signature{
scheme: a.scheme,
data: sigData,
}, nil
}
func (a *cggmp21Aggregator) VerifyShare(message []byte, share cryptothreshold.SignatureShare, publicShare []byte) error {
shareBytes := share.Bytes()
if len(shareBytes) < 64 {
return errors.New("invalid signature share: too short, expected at least 64 bytes (r||s_i)")
}
// Parse the partial signature: r (32 bytes) || s_i (32 bytes)
r := new(big.Int).SetBytes(shareBytes[:32])
si := new(big.Int).SetBytes(shareBytes[32:64])
// secp256k1 curve order
curveParams := secp256k1.S256().Params()
n := curveParams.N
// Range checks: r and s_i must be in [1, n-1]
one := big.NewInt(1)
nMinusOne := new(big.Int).Sub(n, one)
if r.Cmp(one) < 0 || r.Cmp(nMinusOne) > 0 {
return errors.New("invalid signature share: r out of range [1, n-1]")
}
if si.Cmp(one) < 0 || si.Cmp(nMinusOne) > 0 {
return errors.New("invalid signature share: s_i out of range [1, n-1]")
}
// If a public key share Y_i is provided (65-byte uncompressed point),
// perform ECDSA verification of (r, s_i) against Y_i.
// This uses the standard ECDSA check:
// R' = (m * s_i^{-1}) * G + (r * s_i^{-1}) * Y_i
// valid iff R'.x == r (mod n)
if len(publicShare) == 65 && publicShare[0] == 0x04 {
pk := bytesToEllipticPublicKey(publicShare)
if pk == nil {
return errors.New("invalid public share: failed to parse uncompressed point")
}
pk.Curve = secp256k1.S256()
// s_i^{-1} mod n
siInv := new(big.Int).ModInverse(si, n)
if siInv == nil {
return errors.New("invalid signature share: s_i has no modular inverse")
}
// Hash the message if not already 32 bytes
var hash []byte
if len(message) == 32 {
hash = message
} else {
h := sha256.Sum256(message)
hash = h[:]
}
m := new(big.Int).SetBytes(hash)
// u1 = m * s_i^{-1} mod n
u1 := new(big.Int).Mul(m, siInv)
u1.Mod(u1, n)
// u2 = r * s_i^{-1} mod n
u2 := new(big.Int).Mul(r, siInv)
u2.Mod(u2, n)
// R' = u1*G + u2*Y_i (using secp256k1 curve arithmetic)
curve := secp256k1.S256()
x1, y1 := curve.ScalarBaseMult(u1.Bytes())
x2, y2 := curve.ScalarMult(pk.X, pk.Y, u2.Bytes())
rx, _ := curve.Add(x1, y1, x2, y2)
if rx == nil {
return errors.New("invalid signature share: ECDSA check resulted in point at infinity")
}
// Check r == R'.x mod n
rx.Mod(rx, n)
if rx.Cmp(r) != 0 {
return fmt.Errorf("invalid signature share from party %d: ECDSA verification failed (r mismatch)", share.Index())
}
}
return nil
}
type cggmp21Signature struct {
scheme *CGGMP21Scheme
data []byte
}
func (s *cggmp21Signature) Bytes() []byte { return s.data }
func (s *cggmp21Signature) SchemeID() cryptothreshold.SchemeID { return cryptothreshold.SchemeCMP }
type cggmp21Verifier struct {
scheme *CGGMP21Scheme
groupKey cryptothreshold.PublicKey
}
func (v *cggmp21Verifier) GroupKey() cryptothreshold.PublicKey { return v.groupKey }
func (v *cggmp21Verifier) Verify(message []byte, signature cryptothreshold.Signature) bool {
return v.VerifyBytes(message, signature.Bytes())
}
func (v *cggmp21Verifier) VerifyBytes(message, signature []byte) bool {
if len(signature) < 64 {
return false
}
r := new(big.Int).SetBytes(signature[:32])
s := new(big.Int).SetBytes(signature[32:64])
curve := secp256k1.S256()
n := curve.Params().N
if r.Sign() <= 0 || r.Cmp(n) >= 0 || s.Sign() <= 0 || s.Cmp(n) >= 0 {
return false
}
groupKeyBytes := v.groupKey.Bytes()
pk := bytesToEllipticPublicKey(groupKeyBytes)
if pk == nil {
return false
}
pk.Curve = curve
var hash []byte
if len(message) == 32 {
hash = message
} else {
h := sha256.Sum256(message)
hash = h[:]
}
// Standard ECDSA verification
sInv := new(big.Int).ModInverse(s, n)
if sInv == nil {
return false
}
e := new(big.Int).SetBytes(hash)
u1 := new(big.Int).Mul(e, sInv)
u1.Mod(u1, n)
u2 := new(big.Int).Mul(r, sInv)
u2.Mod(u2, n)
x1, y1 := curve.ScalarBaseMult(u1.Bytes())
x2, y2 := curve.ScalarMult(pk.X, pk.Y, u2.Bytes())
rx, _ := curve.Add(x1, y1, x2, y2)
if rx == nil {
return false
}
rx.Mod(rx, n)
return rx.Cmp(r) == 0
}
// =============================================================================
// FROST Scheme (Threshold EdDSA/Schnorr)
// =============================================================================
// FROSTScheme implements threshold.Scheme for FROST (threshold EdDSA).
type FROSTScheme struct {
protocol protocol.Protocol
once sync.Once
}
func (s *FROSTScheme) init() {
s.once.Do(func() {
s.protocol = frost.NewFROSTProtocol()
})
}
func (s *FROSTScheme) ID() cryptothreshold.SchemeID {
return cryptothreshold.SchemeFROST
}
func (s *FROSTScheme) Name() string {
return "FROST (Threshold EdDSA/Schnorr)"
}
func (s *FROSTScheme) KeyShareSize() int {
return 64 // Ed25519 scalar + metadata
}
func (s *FROSTScheme) SignatureShareSize() int {
return 32 // Schnorr signature share
}
func (s *FROSTScheme) SignatureSize() int {
return 64 // Ed25519 signature
}
func (s *FROSTScheme) PublicKeySize() int {
return 32 // Ed25519 public key
}
func (s *FROSTScheme) NewDKG(config cryptothreshold.DKGConfig) (cryptothreshold.DKG, error) {
if err := config.Validate(); err != nil {
return nil, err
}
s.init()
return &frostDKG{
scheme: s,
config: config,
}, nil
}
func (s *FROSTScheme) NewTrustedDealer(config cryptothreshold.DealerConfig) (cryptothreshold.TrustedDealer, error) {
if err := config.Validate(); err != nil {
return nil, err
}
s.init()
return &frostTrustedDealer{
scheme: s,
config: config,
}, nil
}
func (s *FROSTScheme) NewSigner(share cryptothreshold.KeyShare) (cryptothreshold.Signer, error) {
if share == nil {
return nil, errors.New("nil key share")
}
s.init()
return &frostSigner{
scheme: s,
share: share,
}, nil
}
func (s *FROSTScheme) NewAggregator(groupKey cryptothreshold.PublicKey) (cryptothreshold.Aggregator, error) {
if groupKey == nil {
return nil, errors.New("nil group key")
}
s.init()
return &frostAggregator{
scheme: s,
groupKey: groupKey,
}, nil
}
func (s *FROSTScheme) NewVerifier(groupKey cryptothreshold.PublicKey) (cryptothreshold.Verifier, error) {
if groupKey == nil {
return nil, errors.New("nil group key")
}
s.init()
return &frostVerifier{
scheme: s,
groupKey: groupKey,
}, nil
}
func (s *FROSTScheme) ParseKeyShare(data []byte) (cryptothreshold.KeyShare, error) {
return &frostKeyShare{
scheme: s,
data: data,
}, nil
}
func (s *FROSTScheme) ParsePublicKey(data []byte) (cryptothreshold.PublicKey, error) {
return &frostPublicKey{
scheme: s,
data: data,
}, nil
}
func (s *FROSTScheme) ParseSignatureShare(data []byte) (cryptothreshold.SignatureShare, error) {
return &frostSignatureShare{
scheme: s,
data: data,
}, nil
}
func (s *FROSTScheme) ParseSignature(data []byte) (cryptothreshold.Signature, error) {
return &frostSignature{
scheme: s,
data: data,
}, nil
}
// FROST implementation types (similar structure to CGGMP21)
type frostDKG struct {
scheme *FROSTScheme
config cryptothreshold.DKGConfig
party protocol.Party
groupKey cryptothreshold.PublicKey
}
func (d *frostDKG) Round1(ctx context.Context) (cryptothreshold.DKGMessage, error) {
partyIDs := make([]string, d.config.TotalParties)
for i := 0; i < d.config.TotalParties; i++ {
partyIDs[i] = fmt.Sprintf("frost-party-%d", i)
}
selfID := partyIDs[d.config.PartyIndex]
party, err := d.scheme.protocol.KeyGen(selfID, partyIDs, d.config.Threshold)
if err != nil {
return nil, err
}
d.party = party
select {
case msg := <-party.Messages():
return &frostDKGMessage{round: 1, from: d.config.PartyIndex, data: msg.GetData()}, nil
case err := <-party.Errors():
return nil, err
case <-ctx.Done():
return nil, ctx.Err()
}
}
func (d *frostDKG) Round2(ctx context.Context, round1Messages map[int]cryptothreshold.DKGMessage) (cryptothreshold.DKGMessage, error) {
for _, msg := range round1Messages {
if err := d.party.Update(&protocolMessage{data: msg.Bytes()}); err != nil {
return nil, err
}
}
select {
case msg := <-d.party.Messages():
return &frostDKGMessage{round: 2, from: d.config.PartyIndex, data: msg.GetData()}, nil
case err := <-d.party.Errors():
return nil, err
case <-ctx.Done():
return nil, ctx.Err()
}
}
func (d *frostDKG) Round3(ctx context.Context, round2Messages map[int]cryptothreshold.DKGMessage) (cryptothreshold.KeyShare, error) {
for _, msg := range round2Messages {
if err := d.party.Update(&protocolMessage{data: msg.Bytes()}); err != nil {
return nil, err
}
}
for !d.party.Done() {
select {
case err := <-d.party.Errors():
return nil, err
case <-ctx.Done():
return nil, ctx.Err()
default:
}
}
result, err := d.party.Result()
if err != nil {
return nil, err
}
config, ok := result.(protocol.KeyGenConfig)
if !ok {
return nil, errors.New("unexpected result type")
}
data, err := config.Serialize()
if err != nil {
return nil, err
}
return &frostKeyShare{
scheme: d.scheme,
data: data,
index: d.config.PartyIndex,
threshold: d.config.Threshold,
totalParties: d.config.TotalParties,
}, nil
}
func (d *frostDKG) NumRounds() int { return 3 }
func (d *frostDKG) GroupKey() cryptothreshold.PublicKey { return d.groupKey }
type frostTrustedDealer struct {
scheme *FROSTScheme
config cryptothreshold.DealerConfig
}
func (d *frostTrustedDealer) GenerateShares(ctx context.Context) ([]cryptothreshold.KeyShare, cryptothreshold.PublicKey, error) {
rand := d.config.Rand
if rand == nil {
rand = defaultRand()
}
shares := make([]cryptothreshold.KeyShare, d.config.TotalParties)
for i := 0; i < d.config.TotalParties; i++ {
shareData := make([]byte, 32)
if _, err := io.ReadFull(rand, shareData); err != nil {
return nil, nil, err
}
shares[i] = &frostKeyShare{
scheme: d.scheme,
data: shareData,
index: i,
threshold: d.config.Threshold,
totalParties: d.config.TotalParties,
}
}
groupKeyData := make([]byte, 32)
if _, err := io.ReadFull(rand, groupKeyData); err != nil {
return nil, nil, err
}
groupKey := &frostPublicKey{scheme: d.scheme, data: groupKeyData}
for i := range shares {
shares[i].(*frostKeyShare).groupKey = groupKey
}
return shares, groupKey, nil
}
type frostKeyShare struct {
scheme *FROSTScheme
data []byte
index int
threshold int
totalParties int
groupKey cryptothreshold.PublicKey
publicShare []byte
}
func (s *frostKeyShare) Index() int { return s.index }
func (s *frostKeyShare) GroupKey() cryptothreshold.PublicKey { return s.groupKey }
func (s *frostKeyShare) PublicShare() []byte { return s.publicShare }
func (s *frostKeyShare) Threshold() int { return s.threshold }
func (s *frostKeyShare) TotalParties() int { return s.totalParties }
func (s *frostKeyShare) Bytes() []byte { return s.data }
func (s *frostKeyShare) SchemeID() cryptothreshold.SchemeID { return cryptothreshold.SchemeFROST }
type frostPublicKey struct {
scheme *FROSTScheme
data []byte
}
func (k *frostPublicKey) Bytes() []byte { return k.data }
func (k *frostPublicKey) Equal(other cryptothreshold.PublicKey) bool {
if other == nil {
return false
}
otherBytes := other.Bytes()
if len(k.data) != len(otherBytes) {
return false
}
for i := range k.data {
if k.data[i] != otherBytes[i] {
return false
}
}
return true
}
func (k *frostPublicKey) SchemeID() cryptothreshold.SchemeID { return cryptothreshold.SchemeFROST }
type frostSigner struct {
scheme *FROSTScheme
share cryptothreshold.KeyShare
}
func (s *frostSigner) Index() int { return s.share.Index() }
func (s *frostSigner) PublicShare() []byte { return s.share.PublicShare() }
func (s *frostSigner) KeyShare() cryptothreshold.KeyShare { return s.share }
func (s *frostSigner) NonceGen(ctx context.Context) (cryptothreshold.NonceCommitment, cryptothreshold.NonceState, error) {
// FROST requires nonce pre-generation
nonceData := make([]byte, 32)
if _, err := io.ReadFull(defaultRand(), nonceData); err != nil {
return nil, nil, err
}
commitment := &frostNonceCommitment{from: s.share.Index(), data: nonceData}
state := &frostNonceState{from: s.share.Index(), data: nonceData}
return commitment, state, nil
}
func (s *frostSigner) SignShare(ctx context.Context, message []byte, signers []int, nonce cryptothreshold.NonceState) (cryptothreshold.SignatureShare, error) {
shareData := make([]byte, 32)
copy(shareData, message)
return &frostSignatureShare{
scheme: s.scheme,
index: s.share.Index(),
data: shareData,
}, nil
}
type frostNonceCommitment struct {
from int
data []byte
}
func (c *frostNonceCommitment) Bytes() []byte { return c.data }
func (c *frostNonceCommitment) FromParty() int { return c.from }
type frostNonceState struct {
from int
data []byte
}
func (s *frostNonceState) Bytes() []byte { return s.data }
func (s *frostNonceState) FromParty() int { return s.from }
type frostSignatureShare struct {
scheme *FROSTScheme
index int
data []byte
}
func (s *frostSignatureShare) Index() int { return s.index }
func (s *frostSignatureShare) Bytes() []byte { return s.data }
func (s *frostSignatureShare) SchemeID() cryptothreshold.SchemeID { return cryptothreshold.SchemeFROST }
type frostAggregator struct {
scheme *FROSTScheme
groupKey cryptothreshold.PublicKey
}
func (a *frostAggregator) GroupKey() cryptothreshold.PublicKey { return a.groupKey }
func (a *frostAggregator) Aggregate(ctx context.Context, message []byte, shares []cryptothreshold.SignatureShare, commitments []cryptothreshold.NonceCommitment) (cryptothreshold.Signature, error) {
if len(shares) == 0 {
return nil, errors.New("no shares to aggregate")
}
n := secp256k1.S256().Params().N
firstShare := shares[0].Bytes()
if len(firstShare) < 32 {
return nil, errors.New("invalid FROST share: too short")
}
var rBytes []byte
sAgg := new(big.Int)
if len(firstShare) == 32 {
// Shares are scalar-only s_i; R comes from commitments
if len(commitments) == 0 {
return nil, errors.New("FROST aggregation requires nonce commitments when shares are scalar-only")
}
rBytes = commitments[0].Bytes()
if len(rBytes) > 32 {
rBytes = rBytes[:32]
}
for _, share := range shares {
si := new(big.Int).SetBytes(share.Bytes())
sAgg.Add(sAgg, si)
sAgg.Mod(sAgg, n)
}
} else if len(firstShare) >= 64 {
// Shares contain R_x (32) || s_i (32)
rBytes = make([]byte, 32)
copy(rBytes, firstShare[:32])
for _, share := range shares {
sb := share.Bytes()
if len(sb) < 64 {
return nil, fmt.Errorf("invalid FROST share from party %d: too short", share.Index())
}
for j := 0; j < 32; j++ {
if sb[j] != rBytes[j] {
return nil, fmt.Errorf("share from party %d has different R commitment", share.Index())
}
}
si := new(big.Int).SetBytes(sb[32:64])
sAgg.Add(sAgg, si)
sAgg.Mod(sAgg, n)
}
} else {
return nil, fmt.Errorf("unexpected FROST share size: %d", len(firstShare))
}
// BIP-340 compatible signature: R_x (32) || s (32)
sigData := make([]byte, 64)
if len(rBytes) >= 32 {
copy(sigData[:32], rBytes[:32])
}
sB := sAgg.Bytes()
copy(sigData[64-len(sB):64], sB)
return &frostSignature{scheme: a.scheme, data: sigData}, nil
}
func (a *frostAggregator) VerifyShare(message []byte, share cryptothreshold.SignatureShare, publicShare []byte) error {
shareBytes := share.Bytes()
if len(shareBytes) == 0 {
return errors.New("empty share")
}
// secp256k1 curve order
n := secp256k1.S256().Params().N
curve := secp256k1.S256()
// Extract s_i based on share format:
// 32 bytes: scalar-only s_i
// 64 bytes: R_x (32) || s_i (32)
var si *big.Int
var rBytes []byte
if len(shareBytes) == 32 {
si = new(big.Int).SetBytes(shareBytes)
} else if len(shareBytes) >= 64 {
rBytes = shareBytes[:32]
si = new(big.Int).SetBytes(shareBytes[32:64])
} else {
return fmt.Errorf("invalid FROST share size: %d (expected 32 or 64)", len(shareBytes))
}
// Range check: s_i must be in [0, n-1]
if si.Sign() < 0 || si.Cmp(n) >= 0 {
return errors.New("invalid FROST share: s_i out of range [0, n)")
}
// Verify s_i * G is a valid non-identity curve point
siGx, siGy := curve.ScalarBaseMult(si.Bytes())
if siGx.Sign() == 0 && siGy.Sign() == 0 {
return errors.New("invalid FROST share: s_i * G is the point at infinity")
}
// If we have the aggregate R, group key P, and party's public share Y_i,
// verify using the Schnorr partial verification equation:
// s_i * G == R_i + c * Y_i
// where c = tagged_hash("BIP0340/challenge", R_x || P_x || m)
// and R_i is the party's individual nonce commitment.
//
// We compute R_i = s_i * G - c * Y_i and verify it is a valid curve point.
// The caller can later verify that sum(R_i) produces the aggregate R.
if rBytes != nil && len(publicShare) > 0 && a.groupKey != nil {
groupKeyBytes := a.groupKey.Bytes()
var pkXBytes []byte
switch len(groupKeyBytes) {
case 32:
pkXBytes = groupKeyBytes
case 33:
pkXBytes = groupKeyBytes[1:]
case 65:
pkXBytes = groupKeyBytes[1:33]
}
if pkXBytes != nil {
// BIP-340 tagged hash for challenge:
// c = H("BIP0340/challenge", R_x || P_x || m) mod n
tagHash := sha256.Sum256([]byte("BIP0340/challenge"))
h := sha256.New()
h.Write(tagHash[:])
h.Write(tagHash[:])
h.Write(rBytes)
h.Write(pkXBytes)
h.Write(message)
cHash := h.Sum(nil)
c := new(big.Int).SetBytes(cHash)
c.Mod(c, n)
// Parse Y_i (party's verification share)
var yiX, yiY *big.Int
switch len(publicShare) {
case 33:
// Compressed point
pubKey, err := secp256k1.ParsePubKey(publicShare)
if err == nil {
yiX = pubKey.X()
yiY = pubKey.Y()
}
case 65:
if publicShare[0] == 0x04 {
yiX = new(big.Int).SetBytes(publicShare[1:33])
yiY = new(big.Int).SetBytes(publicShare[33:65])
}
case 32:
// x-only key: lift to point with even Y
pubKey, err := secp256k1.ParsePubKey(append([]byte{0x02}, publicShare...))
if err == nil {
yiX = pubKey.X()
yiY = pubKey.Y()
}
}
if yiX != nil && yiY != nil {
// R_i = s_i * G - c * Y_i
cYx, cYy := curve.ScalarMult(yiX, yiY, c.Bytes())
// Negate c*Y_i: -(x,y) = (x, -y mod p)
p := curve.Params().P
cYyNeg := new(big.Int).Sub(p, cYy)
cYyNeg.Mod(cYyNeg, p)
riX, riY := curve.Add(siGx, siGy, cYx, cYyNeg)
if riX.Sign() == 0 && riY.Sign() == 0 {
return fmt.Errorf("invalid FROST share from party %d: R_i is point at infinity", share.Index())
}
// Verify R_i is on curve
if !curve.IsOnCurve(riX, riY) {
return fmt.Errorf("invalid FROST share from party %d: R_i not on curve", share.Index())
}
}
}
}
return nil
}
type frostSignature struct {
scheme *FROSTScheme
data []byte
}
func (s *frostSignature) Bytes() []byte { return s.data }
func (s *frostSignature) SchemeID() cryptothreshold.SchemeID { return cryptothreshold.SchemeFROST }
type frostVerifier struct {
scheme *FROSTScheme
groupKey cryptothreshold.PublicKey
}
func (v *frostVerifier) GroupKey() cryptothreshold.PublicKey { return v.groupKey }
func (v *frostVerifier) Verify(message []byte, signature cryptothreshold.Signature) bool {
return v.VerifyBytes(message, signature.Bytes())
}
func (v *frostVerifier) VerifyBytes(message, signature []byte) bool {
if len(signature) != 64 {
return false
}
// BIP-340 Schnorr verification: sig = R_x (32) || s (32)
groupKeyBytes := v.groupKey.Bytes()
var pkXBytes []byte
switch len(groupKeyBytes) {
case 32:
pkXBytes = groupKeyBytes
case 33:
pkXBytes = groupKeyBytes[1:]
case 65:
pkXBytes = groupKeyBytes[1:33]
default:
return false
}
curve := secp256k1.S256()
n := curve.Params().N
// Parse s and verify range
s := new(big.Int).SetBytes(signature[32:64])
if s.Cmp(n) >= 0 {
return false
}
// Lift R from x-only coordinate
Rx := new(big.Int).SetBytes(signature[:32])
Rpk, err := secp256k1.ParsePubKey(append([]byte{0x02}, signature[:32]...))
if err != nil {
return false
}
// Lift P from x-only coordinate (even Y)
Ppk, err := secp256k1.ParsePubKey(append([]byte{0x02}, pkXBytes...))
if err != nil {
return false
}
// BIP-340 challenge: e = tagged_hash("BIP0340/challenge", R_x || P_x || m) mod n
tagHash := sha256.Sum256([]byte("BIP0340/challenge"))
h := sha256.New()
h.Write(tagHash[:])
h.Write(tagHash[:])
h.Write(signature[:32])
h.Write(pkXBytes)
h.Write(message)
eHash := h.Sum(nil)
e := new(big.Int).SetBytes(eHash)
e.Mod(e, n)
// Verify: s*G == R + e*P => s*G - e*P should equal R
sGx, sGy := curve.ScalarBaseMult(s.Bytes())
ePx, ePy := curve.ScalarMult(Ppk.X(), Ppk.Y(), e.Bytes())
// Negate e*P
p := curve.Params().P
ePyNeg := new(big.Int).Sub(p, ePy)
ePyNeg.Mod(ePyNeg, p)
checkX, checkY := curve.Add(sGx, sGy, ePx, ePyNeg)
if checkX == nil || (checkX.Sign() == 0 && checkY.Sign() == 0) {
return false
}
// BIP-340: R must have even Y
if checkY.Bit(0) != 0 {
return false
}
_ = Rpk
return checkX.Cmp(Rx) == 0
}
// =============================================================================
// Helper types
// =============================================================================
type cggmp21DKGMessage struct {
round int
from int
data []byte
}
func (m *cggmp21DKGMessage) Round() int { return m.round }
func (m *cggmp21DKGMessage) FromParty() int { return m.from }
func (m *cggmp21DKGMessage) Bytes() []byte { return m.data }
type frostDKGMessage struct {
round int
from int
data []byte
}
func (m *frostDKGMessage) Round() int { return m.round }
func (m *frostDKGMessage) FromParty() int { return m.from }
func (m *frostDKGMessage) Bytes() []byte { return m.data }
type protocolMessage struct {
data []byte
}
func (m *protocolMessage) GetFrom() string { return "" }
func (m *protocolMessage) GetTo() []string { return nil }
func (m *protocolMessage) GetData() []byte { return m.data }
func (m *protocolMessage) IsBroadcast() bool { return true }
// Helper functions
func defaultRand() io.Reader {
return cryptorand.Reader
}
func ellipticPublicKeyToBytes(pk *ecdsa.PublicKey) []byte {
if pk == nil {
return nil
}
// Uncompressed point format: 0x04 || X || Y
xBytes := pk.X.Bytes()
yBytes := pk.Y.Bytes()
result := make([]byte, 65)
result[0] = 0x04
copy(result[33-len(xBytes):33], xBytes)
copy(result[65-len(yBytes):65], yBytes)
return result
}
func bytesToEllipticPublicKey(data []byte) *ecdsa.PublicKey {
if len(data) != 65 || data[0] != 0x04 {
return nil
}
return &ecdsa.PublicKey{
X: new(big.Int).SetBytes(data[1:33]),
Y: new(big.Int).SetBytes(data[33:65]),
}
}
// CGGMP21 nonce types for presigning
type cggmp21NonceCommitment struct {
from int
data []byte
}
func (c *cggmp21NonceCommitment) Bytes() []byte { return c.data }
func (c *cggmp21NonceCommitment) FromParty() int { return c.from }
type cggmp21NonceState struct {
from int
data []byte
}
func (s *cggmp21NonceState) Bytes() []byte { return s.data }
func (s *cggmp21NonceState) FromParty() int { return s.from }
// lagrangeCoefficientAt0 computes the Lagrange coefficient for party myIdx
// evaluated at x=0 among the set of signers. Party indices are 0-based;
// evaluation points are x_i = i+1 to avoid division by zero.
func lagrangeCoefficientAt0(myIdx int, signers []int, n *big.Int) *big.Int {
xi := big.NewInt(int64(myIdx + 1))
num := big.NewInt(1)
den := big.NewInt(1)
for _, j := range signers {
if j == myIdx {
continue
}
xj := big.NewInt(int64(j + 1))
negXj := new(big.Int).Neg(xj)
negXj.Mod(negXj, n)
num.Mul(num, negXj)
num.Mod(num, n)
diff := new(big.Int).Sub(xi, xj)
diff.Mod(diff, n)
den.Mul(den, diff)
den.Mod(den, n)
}
denInv := new(big.Int).ModInverse(den, n)
if denInv == nil {
return big.NewInt(0)
}
lambda := new(big.Int).Mul(num, denInv)
lambda.Mod(lambda, n)
return lambda
}
// compressPointSecp256k1 encodes a secp256k1 point as 33-byte compressed form.
func compressPointSecp256k1(x, y *big.Int) []byte {
result := make([]byte, 33)
if y.Bit(0) == 0 {
result[0] = 0x02
} else {
result[0] = 0x03
}
xBytes := x.Bytes()
copy(result[33-len(xBytes):33], xBytes)
return result
}
// decompressPointSecp256k1 decodes a 33-byte compressed secp256k1 point.
func decompressPointSecp256k1(data []byte) (x, y *big.Int) {
if len(data) != 33 || (data[0] != 0x02 && data[0] != 0x03) {
return nil, nil
}
pk, err := secp256k1.ParsePubKey(data)
if err != nil {
return nil, nil
}
return pk.X(), pk.Y()
}