dkg

package module
v0.0.0-...-183ce3f Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 15, 2026 License: MIT Imports: 12 Imported by: 0

README

Distributed Key Generation

dkg Go Reference codecov

  import "github.com/bytemare/dkg"

Package dkg provides an efficient distributed key generation system in Go, easy to use. It builds on the 2-round Pedersen DKG and extends it with zero-knowledge proofs to protect against rogue-key attacks of Byzantine participants, as defined in FROST. This is secure for any t among n participants in a (t,n)-threshold scheme.

This effectively generates keys among participants without the need of a trusted dealer or third-party. These keys are generally valid keys, and can be used in FROST and OPRFs.

References:

Documentation Go Reference

You can find the documentation and usage examples in the package doc.

Usage

Requirements
  • All parties are identified with distinct uint16 non-zero IDs in the range [1:maxSigners].
  • threshold and maxSigners must be non-zero, and threshold must be at most maxSigners.
  • Communicate over confidential, authenticated, and secure channels.
  • All participants honestly follow the protocol (they can, nevertheless, identify a misbehaving participant).
Setup

Use the same ciphersuite for the DKG setup and the key usage in other protocol executions.

Error handling

In case of an identified misbehaving participant, abort the protocol immediately. If this happens there might be a serious problem that must be investigated. One may re-run the protocol after excluding that participant and solving the problem.

Protocol

The following steps describe how to run the DKG among participants. Participants maintain state between phases: Start(), Continue(), and Finalize() are one-shot state transitions on the same Participant instance. Failed calls do not advance state and can be retried with corrected input. For each participant:

  1. Create the participant with NewParticipant(id, threshold, maxSigners).
  2. Run Start()
    • this returns a round 1 package
    • send/broadcast this package to every other participant (this might include the very same participant, in which case it will discard it)
  3. Collect all the round 1 packages from other participants
  4. Run Continue() with the collection of round 1 packages
    • this verifies the round 1 proofs of knowledge and stores the verified commitments
    • this returns round 2 packages, one destined to each other participant
    • each package specifies the intended receiver
    • Round2Data contains secret shares; send it only to the intended receiver over an authenticated confidential transport and never broadcast or log it
  5. Collect all round 2 packages destined to the participant
  6. Run Finalize() with the collected round 1 and round 2 packages
    • provide the same round-1 commitments accepted by Continue(); proofs may be cleared after Continue()
    • returns the participant's own secret signing share, the corresponding verification/public share, and the group's public key
  7. Optionally compute the group verification key from round 1 with VerificationKeyFromRound1() before clearing round-1 proofs. After proofs are cleared, use already validated commitments or finalized public key shares.
  8. Erase all intermediary values received and computed by the participants (including in their states)
  9. You might want each participant to already send their PublicKeyShare to a central coordinator or broadcast it to the other participants, as required to run the FROST protocol.

Versioning

SemVer is used for versioning. For the versions available, see the tags on the repository.

Release Integrity (SLSA Level 3)

Releases are built with the reusable bytemare/slsa workflow and ship the evidence required for SLSA Level 3 compliance:

  • 📦 Artifacts are uploaded to the release page, and include the deterministic source archive plus subjects.sha256, signed SBOM (sbom.cdx.json), GitHub provenance (*.intoto.jsonl), a reproducibility report (verification.json), and a signed Verification Summary Attestation (verification-summary.attestation.json[.bundle]).
  • ✍️ All artifacts are signed using Sigstore with transparency via Rekor.
  • ✅ Verification (or see the latest docs at bytemare/slsa):
curl -sSL https://raw.githubusercontent.com/bytemare/slsa/main/verify-release.sh -o verify-release.sh
chmod +x verify-release.sh
./verify-release.sh --repo <owner>/<repo> --tag <tag> --mode full --signer-repo bytemare/slsa

Run again with --mode reproduce to build in a container, or --mode vsa to validate just the verification summary.

Contributing

Please read CONTRIBUTING.md for details on the code of conduct, and the process for submitting pull requests.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Documentation

Overview

Package dkg implements the Distributed Key Generation described in FROST, using zero-knowledge proofs in Schnorr signatures.

Example (Dkg)

Example_dkg shows the 3-step 2-message distributed key generation procedure that must be executed by each participant to build their secret key share.

package main

import (
	"fmt"

	"github.com/bytemare/ecc"
	"github.com/bytemare/secret-sharing/keys"

	"github.com/bytemare/dkg"

	secretsharing "github.com/bytemare/secret-sharing"
)

func main() {
	// Each participant must be set to use the same configuration. We use (3,5) here for the demo, on Ristretto255.
	totalAmountOfParticipants := uint16(5)
	threshold := uint16(3)
	c := dkg.Ristretto255Sha512

	var err error

	// Step 0: Initialise your participant. Each participant must be given an identifier that MUST be unique among
	// all participants. For this example, The participants will have the identifiers 1, 2, 3, 4, and 5.
	participants := make([]*dkg.Participant, totalAmountOfParticipants)
	for id := uint16(1); id <= totalAmountOfParticipants; id++ {
		participants[id-1], err = c.NewParticipant(id, threshold, totalAmountOfParticipants)
		if err != nil {
			panic(err)
		}
	}

	// Step 1: Call Start() on each participant. This will return data that must be broadcast to all other participants
	// over an authenticated channel, which can be encoded/serialized to send over the network. The proxy coordinator or every
	// participant must compile all these packages so that all have the same set.
	accumulatedRound1DataBytes := make([][]byte, totalAmountOfParticipants)
	for i, p := range participants {
		r1, err := p.Start()
		if err != nil {
			panic(err)
		}
		accumulatedRound1DataBytes[i] = r1.Encode()
	}

	// Upon reception of the encoded set, decode each item.
	decodedRound1Data := make([]*dkg.Round1Data, totalAmountOfParticipants)
	for i, data := range accumulatedRound1DataBytes {
		decodedRound1Data[i] = new(dkg.Round1Data)
		if err = decodedRound1Data[i].Decode(data); err != nil {
			panic(err)
		}
	}

	// Step 2: Call Continue() on each participant providing them with the compiled decoded data. Each participant will
	// return a map of Round2Data, one for each other participant. Round2Data carries secret shares, so send each item
	// only to its intended peer over an authenticated confidential transport and never broadcast or log it.
	accumulatedRound2Data := make([]map[uint16]*dkg.Round2Data, totalAmountOfParticipants)
	for i, p := range participants {
		if accumulatedRound2Data[i], err = p.Continue(decodedRound1Data); err != nil {
			panic(err)
		}
	}

	// We'll skip the encoding/decoding part (each Round2Data item can be encoded and sent to the intended recipient).
	// Step 3: Each participant receives the Round2Data set destined to them (there's a Receiver identifier in each
	// Round2Data item), and then calls Finalize with the Round1 and their Round2 data. This will output the
	// participant's key share, containing its secret, public key share, and the group's public key that can be used for
	// signature verification.
	keyShares := make([]*keys.KeyShare, totalAmountOfParticipants)
	for i, p := range participants {
		accumulatedRound2DataForParticipant := make([]*dkg.Round2Data, 0, totalAmountOfParticipants)
		for _, r2Data := range accumulatedRound2Data {
			if d := r2Data[p.Identifier]; d != nil && d.RecipientIdentifier == p.Identifier {
				accumulatedRound2DataForParticipant = append(accumulatedRound2DataForParticipant, d)
			}
		}

		if keyShares[i], err = p.Finalize(decodedRound1Data, accumulatedRound2DataForParticipant); err != nil {
			panic(err)
		}
	}

	// Optional: Each participant can extract their public info pks := keyShare.Public() and send it to others
	// or a registry of participants. You can encode the registry for transmission or storage (in byte strings or JSON),
	// and recover it.
	publicKeyShares := make([]*keys.PublicKeyShare, len(keyShares))
	for i, ks := range keyShares {
		// A participant extracts its public key share and sends it to the others or the coordinator.
		publicKeyShares[i] = ks.PublicKeyShare()
	}

	// Anyone can maintain a registry for a complete setup.
	PublicKeyShareRegistry, err := keys.NewPublicKeyShareRegistry(
		c.Group(),
		threshold,
		totalAmountOfParticipants,
		keyShares[0].VerificationKey(),
		publicKeyShares,
	)
	if err != nil {
		panic(err)
	}

	// A complete validated registry checks every finalized public key of the setup.
	for _, pks := range PublicKeyShareRegistry.Shares() {
		if err = PublicKeyShareRegistry.ContainsPublicKey(pks.Identifier(), pks.PublicKey()); err != nil {
			panic(err)
		}
	}

	// Optional: There are multiple ways on how you can get the group's public key (the one used for signature validation)
	// 1. Participant's Finalize() function returns a KeyShare, which contains the VerificationKey, which can be sent to
	// the coordinator or registry.
	// 2. Using the Round1 data before proofs are cleared, this is convenient during protocol execution.
	// 3. Using the participants' commitments in their public key share, this is convenient after protocol execution.
	verificationKey1 := keyShares[0].VerificationKey()
	verificationKey2, err := dkg.VerificationKeyFromRound1(c, decodedRound1Data)
	if err != nil {
		panic(err)
	}
	verificationKey3, err := dkg.VerificationKeyFromCommitments(
		c,
		[][]*ecc.Element{dkg.VSSCommitmentFromRegistry(PublicKeyShareRegistry)},
	)
	if err != nil {
		panic(err)
	}

	if !verificationKey1.Equal(verificationKey2) || !verificationKey2.Equal(verificationKey3) {
		panic("group public key recovery failed")
	}

	// A registry can be encoded for backup or transmission.
	encodedRegistry := PublicKeyShareRegistry.Encode()
	fmt.Printf("The encoded registry of public keys is %d bytes long.\n", len(encodedRegistry))

	// Optional: This is how a participant can verify any participants public key of the protocol, given all the commitments.
	// This can be done with the Commitments in the Round1 data set or in the collection of public key shares.
	publicKeyShare := keyShares[2].PublicKeyShare()
	if err = PublicKeyShareRegistry.ContainsPublicKey(publicKeyShare.Identifier(), publicKeyShare.PublicKey()); err != nil {
		panic(err)
	}

	fmt.Printf("Signing keys for participant set up and valid.")

	// Not recommended, but shown for consistency: if you gather at least threshold amount of secret keys from participants,
	// you can reconstruct the private key, and validate it with the group's public key. In our example, we use only
	// one participant, so the keys are equivalent. In a true setup, you don't want to extract and gather participants'
	// private keys, as it defeats the purpose of a DKG and might expose them.
	g := c.Group()
	shares := make(
		[]*keys.KeyShare,
		threshold,
	) // Here you would add the secret keys from the other participants.
	for i, k := range keyShares[:threshold] {
		shares[i] = k
	}

	recombinedSecret, err := secretsharing.CombineShares(shares, threshold)
	if err != nil {
		panic("failed to reconstruct secret")
	}

	groupPubKey := g.Base().Multiply(recombinedSecret)
	if !groupPubKey.Equal(verificationKey3) {
		panic("failed to recover the correct group secret")
	}

}
Output:
The encoded registry of public keys is 702 bytes long.
Signing keys for participant set up and valid.

Index

Examples

Constants

View Source
const (
	// Ristretto255Sha512 identifies the Ristretto255 group and SHA-512.
	Ristretto255Sha512 = Ciphersuite(ecc.Ristretto255Sha512)

	// P256Sha256 identifies the NIST P-256 group and SHA-256.
	P256Sha256 = Ciphersuite(ecc.P256Sha256)

	// P384Sha384 identifies the NIST P-384 group and SHA-384.
	P384Sha384 = Ciphersuite(ecc.P384Sha384)

	// P521Sha512 identifies the NIST P-512 group and SHA-512.
	P521Sha512 = Ciphersuite(ecc.P521Sha512)

	// Edwards25519Sha512 identifies the Edwards25519 group and SHA2-512.
	Edwards25519Sha512 = Ciphersuite(ecc.Edwards25519Sha512)

	// Secp256k1 identifies the SECp256k1 group and SHA-256.
	Secp256k1 = Ciphersuite(ecc.Secp256k1Sha256)
)

Variables

This section is empty.

Functions

func ComputeParticipantPublicKey

func ComputeParticipantPublicKey(c Ciphersuite, id uint16, commitments [][]*ecc.Element) (*ecc.Element, error)

ComputeParticipantPublicKey computes the verification share for participant id given the commitments of round 1.

func FrostVerifyZeroKnowledgeProof

func FrostVerifyZeroKnowledgeProof(c Ciphersuite, id uint16, pubkey *ecc.Element, proof *Signature) (bool, error)

FrostVerifyZeroKnowledgeProof verifies a proof generated by FrostGenerateZeroKnowledgeProof.

func VSSCommitmentFromRegistry

func VSSCommitmentFromRegistry(registry *keys.PublicKeyShareRegistry) []*ecc.Element

VSSCommitmentFromRegistry returns the aggregate commitment for a complete registry.

func VSSCommitmentsFromRegistry deprecated

func VSSCommitmentsFromRegistry(registry *keys.PublicKeyShareRegistry) [][]*ecc.Element

VSSCommitmentsFromRegistry returns the aggregate commitment for a complete registry.

Deprecated: use VSSCommitmentFromRegistry.

func VerificationKeyFromCommitments

func VerificationKeyFromCommitments(c Ciphersuite, commitments [][]*ecc.Element) (*ecc.Element, error)

VerificationKeyFromCommitments returns the threshold setup's group public key from participant commitments. It assumes those commitments came from an already validated DKG transcript or another trusted source, because it does not verify the Round 1 proofs of knowledge.

func VerificationKeyFromRound1

func VerificationKeyFromRound1(c Ciphersuite, r1DataSet []*Round1Data) (*ecc.Element, error)

VerificationKeyFromRound1 returns the global public key, usable to verify signatures produced in a threshold scheme. It validates each Round 1 commitment and proof of knowledge, so it must be called before proofs are cleared.

func VerifyPublicKey

func VerifyPublicKey(c Ciphersuite, id uint16, pubKey *ecc.Element, commitments [][]*ecc.Element) error

VerifyPublicKey verifies if the pubKey associated to id is valid given the public VSS commitments of the other participants.

Types

type Ciphersuite

type Ciphersuite byte

A Ciphersuite defines the elliptic curve group to use.

func (Ciphersuite) Available

func (c Ciphersuite) Available() bool

Available returns whether the Ciphersuite is supported, useful to avoid casting to an unsupported group identifier.

func (Ciphersuite) Group

func (c Ciphersuite) Group() ecc.Group

Group returns the elliptic curve group used in the ciphersuite.

func (Ciphersuite) NewParticipant

func (c Ciphersuite) NewParticipant(
	id uint16,
	threshold, maxSigners uint16,
	polynomial ...*ecc.Scalar,
) (*Participant, error)

NewParticipant instantiates a new participant with identifier id. The identifier must be non-zero and unique among the set of participants. maxSigners and threshold must be non-zero, and threshold must be at most maxSigners.

The same Participant instance must be used throughout the protocol execution, because it stores the validated intermediary values between Start, Continue, and Finalize. Optionally, the participant's secret polynomial can be provided to set its secret and commitment, which enables re-instantiating the same participant if the same polynomial is used. A provided polynomial must have exactly threshold coefficients, valid same-group scalar coefficients, a non-zero secret coefficient, and a non-zero highest-degree coefficient. Interior zero coefficients and repeated coefficient values are valid.

type Participant

type Participant struct {
	Identifier uint16
	// contains filtered or unexported fields
}

Participant represents a party in the Distributed Key Generation. A Participant is stateful: Start, Continue, and Finalize are one-shot protocol phases on the same instance. Once the DKG is complete, all intermediary values must be erased.

func (*Participant) Continue

func (p *Participant) Continue(r1DataSet []*Round1Data) (map[uint16]*Round2Data, error)

Continue ingests the broadcast data from other peers, verifies their proofs of knowledge, stores the verified commitments for Finalize, and returns one Round2Data package for each peer.

func (*Participant) Finalize

func (p *Participant) Finalize(r1DataSet []*Round1Data, r2DataSet []*Round2Data) (*keys.KeyShare, error)

Finalize ingests the same round 1 commitments accepted by Continue and the round 2 data destined for the participant, then returns the participant's secret share, verification key, and the group's public key. Round 1 proofs may be cleared after Continue, but the commitments must still match the verified transcript.

func (*Participant) Start

func (p *Participant) Start() (*Round1Data, error)

Start returns a participant's output for the first round and advances the participant to the started state.

func (*Participant) StartWithRandom

func (p *Participant) StartWithRandom(random *ecc.Scalar) (*Round1Data, error)

StartWithRandom returns a participant's output for the first round and allows setting the Schnorr proof nonce used by the NIZK proof. Omit random in normal use; it must stay secret and be unique for a given secret across distinct challenges, because reuse or disclosure can leak the secret.

type Round1Data

type Round1Data struct {
	ProofOfKnowledge *Signature     `json:"proof"`
	Commitment       []*ecc.Element `json:"commitment"`
	SenderIdentifier uint16         `json:"senderId"`
	Group            ecc.Group      `json:"group"`
}

Round1Data is the output data of the Start() function, to be broadcast to all participants. Keep ProofOfKnowledge intact until all participants have called Continue and until VerificationKeyFromRound1 has been called, if used.

func (*Round1Data) Decode

func (d *Round1Data) Decode(data []byte) error

Decode deserializes a valid byte encoding of Round1Data.

func (*Round1Data) DecodeHex

func (d *Round1Data) DecodeHex(h string) error

DecodeHex sets k to the decoding of the hex encoded representation returned by Hex().

func (*Round1Data) Encode

func (d *Round1Data) Encode() []byte

Encode returns a compact byte serialization of Round1Data. It returns nil for nil or malformed values.

func (*Round1Data) Hex

func (d *Round1Data) Hex() string

Hex returns the hexadecimal representation of the byte encoding returned by Encode(). It returns an empty string when Encode returns nil.

func (*Round1Data) UnmarshalJSON

func (d *Round1Data) UnmarshalJSON(data []byte) error

UnmarshalJSON reads the input data as JSON and deserializes it into the receiver. It doesn't modify the receiver when encountering an error.

type Round2Data

type Round2Data struct {
	SecretShare         *ecc.Scalar `json:"secretShare"`
	SenderIdentifier    uint16      `json:"senderId"`
	RecipientIdentifier uint16      `json:"recipientId"`
	Group               ecc.Group   `json:"group"`
}

Round2Data is an output of the Continue() function, to be sent only to RecipientIdentifier. It contains secret share material and must be sent over authenticated confidential transport; never broadcast or log it.

func (*Round2Data) Decode

func (d *Round2Data) Decode(data []byte) error

Decode deserializes a valid byte encoding of Round2Data.

func (*Round2Data) DecodeHex

func (d *Round2Data) DecodeHex(h string) error

DecodeHex sets k to the decoding of the hex encoded representation returned by Hex().

func (*Round2Data) Encode

func (d *Round2Data) Encode() []byte

Encode returns a compact byte serialization of Round2Data. It returns nil for nil or malformed values.

func (*Round2Data) Hex

func (d *Round2Data) Hex() string

Hex returns the hexadecimal representation of the byte encoding returned by Encode(). It returns an empty string when Encode returns nil.

func (*Round2Data) UnmarshalJSON

func (d *Round2Data) UnmarshalJSON(data []byte) error

UnmarshalJSON reads the input data as JSON and deserializes it into the receiver. It doesn't modify the receiver when encountering an error.

type Signature

type Signature struct {
	R     *ecc.Element `json:"r"`
	Z     *ecc.Scalar  `json:"z"`
	Group ecc.Group    `json:"group"`
}

Signature represents a Schnorr signature.

func FrostGenerateZeroKnowledgeProof

func FrostGenerateZeroKnowledgeProof(
	c Ciphersuite,
	id uint16,
	secret *ecc.Scalar,
	pubkey *ecc.Element,
	rand ...*ecc.Scalar,
) (*Signature, error)

FrostGenerateZeroKnowledgeProof generates a zero-knowledge proof of secret, as defined by the FROST protocol. Omit rand in normal use. If provided, exactly one rand value is accepted; it is the Schnorr proof nonce and must stay secret and be unique for a given secret across distinct challenges, because reuse or disclosure can leak the secret.

func (*Signature) Clear

func (s *Signature) Clear()

Clear overwrites the original values with default ones. It is a no-op for nil or malformed signatures.

func (*Signature) Decode

func (s *Signature) Decode(data []byte) error

Decode deserializes the compact encoding obtained from Encode(), or returns an error.

func (*Signature) DecodeHex

func (s *Signature) DecodeHex(h string) error

DecodeHex sets s to the decoding of the hex encoded representation returned by Hex().

func (*Signature) Encode

func (s *Signature) Encode() []byte

Encode serializes the signature into a byte string. It returns nil for nil or malformed values.

func (*Signature) Hex

func (s *Signature) Hex() string

Hex returns the hexadecimal representation of the byte encoding returned by Encode(). It returns an empty string when Encode returns nil.

func (*Signature) UnmarshalJSON

func (s *Signature) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes data into k, or returns an error.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL