frost

package module
v0.0.0-...-f349239 Latest Latest
Warning

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

Go to latest
Published: Oct 8, 2024 License: MIT Imports: 13 Imported by: 1

README

❄ FROST

frost Go Reference codecov

  import "github.com/bytemare/frost"

This package implements RFC9591 - The FROST Flexible Round-Optimized Schnorr Threshold protocol. FROST provides Two-Round Threshold Schnorr Signatures.

The Ristretto255, Edwards25519, Secp256k1, and NIST elliptic curve groups are fully supported.

The FROST Distributed Key Generation protocol produces compatible keys, as described in the original work.

Requirements
  • When communicating at protocol execution, network channels don't need to be confidential but MUST be authenticated. This package verifies a lot of things with regard to the correctness to the protocol, but it assumes that signers and coordinators really communicate with the relevant peer.
  • Long-term fixed configuration values MUST be known to all participant signers and coordinators (i.e. the ciphersuite, threshold and maximum amount of signers, and the public key for signature verification)
  • For every signing session, at least the public key shares of all other participants MUST be known to all participant signers and coordinators (which can be a subset t-among-n of the initial key generation setup)
  • Data provided to these functions (especially when received over the network) MUST be deserialized using the corresponding decoding functions. If data deserialization/decoding fails for a signer, protocol execution must be aborted.
  • Identifiers (for participants/signers) MUST be between 1 and n, which is the maximum amount of participants defined at key generation.
Supported Ciphersuites
ID Name Backend
1 Ristretto255 (recommended) github.com/gtank/ristretto255
3 P-256 filippo.io/nistec
4 P-384 filippo.io/nistec
5 P-521 filippo.io/nistec
6 Edwards25519 filippo.io/edwards25519
7 Secp256k1 github.com/bytemare/secp256k1

The groups, scalars (secret keys and nonces), and group elements (public keys and commitments) are opaque objects that expose all necessary cryptographic and serialization functions. If you have existing cryptographic material in their canonical encodings, they can of course be imported.

Usage

Usage examples and comments can be found in examples_test.go.

Key Generation

The FROST Distributed Key Generation is recommended to produce key material for all participants in the setup. This package also puts out KeyShares and PublicKeyShares ready to use with this FROST implementation. It also ensures correct identifier generation compatible with FROST.

It is heavily recommended to use the same instances for distributed key generation and signing, as this will avoid that the secret key material leaves that instance.

For testing and debugging only, the debug package provides a centralised key generation with a trusted dealer.

Key Management

If the DKG package was used to generate keys, signers can use the produced KeyShare and must communicate their PublicKeyShare to the coordinator and other signers.

It is easy to encode and decode these key shares and public key shares for transmission and storage, using the Encode() and Decode() methods (or hexadecimal or JSON marshalling).

Import existing identifiers and keys

Existing key material (e.g. identifiers, secret public, public keys) that has been generated otherwise (or transmitted or backed up) and encoded in their canonical byte representation can be imported.

To create a KeyShare and PublicKeyShare from individually encoded secret and public keys, use the keys.NewKeyShare() and NewPublicKeyShare() functions, respectively. If a KeyShare or PublicKeyShare have been encoded using their respective Encode() method, they can be easily recovered using the corresponding Decode() method.

More generally, to decode an element (or point) in the Ristretto255 group,

import (
    "https://github.com/bytemare/ecc"
)

bytesPublicKey := []byte{1, 2, 3, ...}

g := ecc.Ristretto255Sha512

publicKey := g.NewElement()
if err := publicKey.Decode(bytesPublicKey); err != nil {
	return fmt.Errorf("can't decode public key: %w", err)
}

The same goes for secret keys (or scalars),

import (
    "https://github.com/bytemare/ecc"
)

bytesSecretKey := []byte{1, 2, 3, ...}

g := ecc.Ristretto255Sha512

secretKey := g.NewScalar()
if err := secretKey.Decode(bytesSecretKey); err != nil {
	return fmt.Errorf("can't decode secret key: %w", err)
}

and any other byte or json encoded structure.

Setup

Both signers and coordinators must first instantiate a Configuration with the long-term fixed values as used at key generation:

  • the ciphersuite (see the frost.Ciphersuite values for available ciphersuites)
  • threshold (t) and maximum amount of signers (n)
  • the global public key for signature verification (as put out at key generation)

Then add the PublicKeyShares of the participants (or signers). For simplicity, it is recommended to add all PublicKeyShares of the all participants from the key generation step. It is sufficient, though, to only use the shares for the signers that will participate in a signing session (which can be a subset t among n).

configuration := &frost.Configuration{
		Ciphersuite:           ciphersuite,
		Threshold:             threshold,
		MaxSigners:            maxSigners,
        VerificationKey:       verificationKey,
		SignerPublicKeyShares: publicKeyShares,
	}

if err := configuration.Init(); err != nil {
    return err
}

This configuration can be encoded for transmission and offline storage, and re-instantiated using its Encode() and Decode() methods. This avoids having to store the parameters separately.

Signers

Once the configuration is initialised, setting up a signer is straightforward, using the Signer() method and providing the signer's KeyShare.

Protocol execution

FROST is a two round signing protocol, in which the first round can be asynchronously pre-computed, so that signing can actually be done in one round when necessary.

First Round: Signer commitment
  • Signers commit to internal nonces, by calling the commitment := signer.Commit() method, which returns one commitment and stores corresponding nonces internally. In this manner, signers can produce many commitments before signing sessions start. Note that a commitment is not function of the future message to sign, so a signer can produce them without knowing the message in advance.
  • Signers send these commitments to either a coordinator or all other signers.
  • The coordinator (or all other signers) collect these commitments, into a list. The coordinator can prepare such lists for each future message to be signed, a list containing a single commitment from each signer. These commitments must not be reused.
Second Round: Signing
  • The coordinator broadcasts the message to be signed and a list of commitments, one from each signer, to each signer.
  • The signers sign the message sigShare, err := signer.Sign(message, commitmentList), each producing their signature share.
  • These signature shares must then be shared and aggregated to produce the final signature, signature, err := configuration.AggregateSignatures(message, sigShares, commitmentList, true).
Coordinator

The coordinator does not have any secret or private information, and must never have. It is also assumed to behave honestly.

Commitments received by signers have an identifier, which allows for triage and registration. Commitments must only be used once. The coordinator may further hedge against nonce-reuse by tracking the nonce commitments used for a given group key.

If the verify argument in the AggregateSignatures() is set to true (which is recommended), signature shares are thoroughly verified. Upon error or invalid share, the error message indicates the first invalid share it encountered. A coordinator should always verify the signature after AggregateSignatures() if the verify argument has been set to false.

If verification fails, the coordinator can then check signature shares individually to deter the misbehaving signer, leveraging the authenticated channel associated to them. That signer can then be denied of further contributions.

Resumption and storage

Configurations, keys, commitments, commitment lists, and even signers can be serialized for transmission and storage, and re-instantiated from them. To decode, just create that object and use its Decode() method.

For example, to back up a signer with its private keys and commitments, use:

bytes := signer.Encode()

To re-instantiate that same signer from the byte string, do:

// bytes := signer.Encode()

signer := new(frost.Signer)
if err := signer.Decode(bytes); err != nil {
	return err
}

Keep in mind that signer encoding embeds the private key and secret nonces, and that they must be secured accordingly.

Notes

Signers have local secret data and state, offline and during protocol execution:

  • the long term secret key

  • the internally stored commitment nonces, maintained between commitment and signature

  • FROST is not robust by design.

    • This means that there is a misbehaving participant if signature aggregation fails (or if the output signature is not valid), in which case the protocol should be aborted and the problem investigated (you shouldn't have a compromised or misbehaving participant in a sane infrastructure).
    • Misbehaving signers can DOS the protocol by providing wrong sig shares or not contributing.
  • The coordinator may further hedge against nonce-reuse by tracking the nonce commitments used for a given group key

  • For message pre-hashing, see RFC

Documentation Go Reference

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

Versioning

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

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 frost implements FROST, the Flexible Round-Optimized Schnorr Threshold (FROST) signing protocol.

Example (Coordinator)

Example_coordinator shows how to aggregate signature shares produced by signers into the final signature and verify a final FROST signature.

package main

import (
	"fmt"

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

	"github.com/bytemare/frost"
	"github.com/bytemare/frost/debug"
)

func main() {
	maxSigners := uint16(5)
	threshold := uint16(3)
	message := []byte("example message")
	ciphersuite := frost.Default

	// We assume you already have a pool of participants with distinct non-zero identifiers and their signing share.
	// The following block uses a centralised trusted dealer to do this, but it is strongly recommended to use
	// distributed key generation, e.g. from github.com/bytemare/dkg, which is compatible with FROST.
	secretKeyShares, verificationKey, _ := debug.TrustedDealerKeygen(ciphersuite, nil, threshold, maxSigners)
	participantSecretKeyShares := secretKeyShares[:threshold]
	participants := make([]*frost.Signer, threshold)

	// At key generation, each participant must send their public key share to the coordinator, and the collection must
	// be broadcast to every participant.
	publicKeyShares := make([]*keys.PublicKeyShare, len(secretKeyShares))
	for i, sk := range secretKeyShares {
		publicKeyShares[i] = sk.Public()
	}

	// This is how to set up the Configuration for FROST, the same for every signer and the coordinator.
	configuration := &frost.Configuration{
		Ciphersuite:           ciphersuite,
		Threshold:             threshold,
		MaxSigners:            maxSigners,
		VerificationKey:       verificationKey,
		SignerPublicKeyShares: publicKeyShares,
	}

	if err := configuration.Init(); err != nil {
		panic(err)
	}

	// Create a participant on each instance
	for i, ks := range participantSecretKeyShares {
		signer, err := configuration.Signer(ks)
		if err != nil {
			panic(err)
		}

		participants[i] = signer
	}

	// Pre-commit
	commitments := make(frost.CommitmentList, threshold)
	for i, p := range participants {
		commitments[i] = p.Commit()
	}

	commitments.Sort()

	// Sign
	signatureShares := make([]*frost.SignatureShare, threshold)
	for i, p := range participants {
		var err error
		signatureShares[i], err = p.Sign(message, commitments)
		if err != nil {
			panic(err)
		}
	}

	// Everything above was a simulation of commitment and signing rounds to produce the signature shares.
	// The following shows how to aggregate these shares, and if verification fails, how to identify a misbehaving signer.

	// The coordinator assembles the shares. If the verify argument is set to true, AggregateSignatures will internally
	// verify each signature share and return an error on the first that is invalid. It will also verify whether the
	// output signature is valid.
	signature, err := configuration.AggregateSignatures(message, signatureShares, commitments, true)
	if err != nil {
		panic(err)
	}

	// Verify the signature and identify potential foul players. Note that since we set verify to true when calling
	// AggregateSignatures, the following is redundant.
	// Anyone can verify the signature given the ciphersuite parameter, message, and the group public key.
	if err = frost.VerifySignature(ciphersuite, message, signature, verificationKey); err != nil {
		// At this point one should try to identify which participant's signature share is invalid and act on it.
		// This verification is done as follows:
		for _, signatureShare := range signatureShares {
			if err := configuration.VerifySignatureShare(signatureShare, message, commitments); err != nil {
				panic(
					fmt.Sprintf(
						"participant %v produced an invalid signature share: %s",
						signatureShare.SignerIdentifier,
						err,
					),
				)
			}
		}

		fmt.Println(err)
		panic("Signature verification failed.")
	}

	fmt.Println("Signature is valid.")

}
Output:

Signature is valid.
Example (Deserialize)

Example_deserialize shows how to encode and decode a FROST messages.

package main

import (
	"bytes"
	"encoding/hex"
	"fmt"

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

	"github.com/bytemare/frost"
)

func main() {
	verificationKeyHex := "74144431f64b052a173c2505e4224a6cc5f3e81d587d4f23369e1b2b1fd0d427"
	publicKeySharesHex := []string{
		"010100000000003c5ff80cd593a3b7e9007fdbc2b8fe6caee380e7d23eb7ba35160a5b7a51cb08",
		"0102000000000002db540a823f17b975d9eb206ccfbcf3a7667a0365ec1918fa2c3bb69acb105c",
		"010300000000008cff0ae1ded90e77095b55218d3632cd90b669d05c888bca26093681e5250870",
	}

	g := frost.Default.Group()
	verificationKey := g.NewElement()
	if err := verificationKey.DecodeHex(verificationKeyHex); err != nil {
		fmt.Println(err)
	}

	publicKeyShares := make([]*keys.PublicKeyShare, len(publicKeySharesHex))
	for i, p := range publicKeySharesHex {
		publicKeyShares[i] = new(keys.PublicKeyShare)
		if err := publicKeyShares[i].DecodeHex(p); err != nil {
			fmt.Println(err)
		}
	}

	// This is how to set up the Configuration for FROST, the same for every signer and the coordinator.
	// Note that every configuration setup for a Signer needs the public key shares of all other signers participating
	// in a signing session (at least for the Sign() step).
	configuration := &frost.Configuration{
		Ciphersuite:           frost.Default,
		Threshold:             2,
		MaxSigners:            3,
		VerificationKey:       verificationKey,
		SignerPublicKeyShares: publicKeyShares,
	}

	// Decoding a commitment.
	commitment1Hex := "01963090de7d665c5101009073f1a30f4fb9a84275206002fc4394aea7a6cbaf944a7b2f0ae" +
		"9143f39fe62808704f776fccfc0080e90e59fdf9bf0156141732728d41fb15554b46a037a40"
	commitment2Hex := "017615b41957cca8d70200c2d3d3e8133d18daf95aee5371f397771118be5f3917058502637" +
		"0fa893828462400bfab522a542010e70b2b6d4eb388f92b47d6e01abbc16ea24aed5b4fb652"

	commitment1 := new(frost.Commitment)
	if err := commitment1.DecodeHex(commitment1Hex); err != nil {
		fmt.Println(err)
	}

	commitment2 := new(frost.Commitment)
	if err := commitment2.DecodeHex(commitment2Hex); err != nil {
		fmt.Println(err)
	}

	// You can individually check a commitment
	if err := configuration.ValidateCommitment(commitment1); err != nil {
		fmt.Println(err)
	}

	// You can then assemble these commitments to build a list.
	commitmentList := make(frost.CommitmentList, 2)
	commitmentList[0] = commitment1
	commitmentList[1] = commitment2

	encodedCommitmentListBytes := commitmentList.Encode()
	encodedCommitmentListHex := hex.EncodeToString(encodedCommitmentListBytes)

	// Note that the commitments are the same, but serializing using a CommitmentList is slightly different (3 bytes more)
	// since it has a length prefix header.
	commitmentListHex := "010200" +
		"01963090de7d665c5101009073f1a30f4fb9a84275206002fc4394aea7a6cbaf944a7b2f0ae" +
		"9143f39fe62808704f776fccfc0080e90e59fdf9bf0156141732728d41fb15554b46a037a40" +
		"017615b41957cca8d70200c2d3d3e8133d18daf95aee5371f397771118be5f3917058502637" +
		"0fa893828462400bfab522a542010e70b2b6d4eb388f92b47d6e01abbc16ea24aed5b4fb652"

	if commitmentListHex != encodedCommitmentListHex {
		fmt.Println(
			"something went wrong when re-encoding the first commitment list, which should yield the same output",
		)
	}

	// Decoding a whole commitment list.
	decodedCommitmentList, err := frost.DecodeList(encodedCommitmentListBytes)
	if err != nil {
		fmt.Println(err)
	}

	reEncodedListBytes := decodedCommitmentList.Encode()
	if !bytes.Equal(reEncodedListBytes, encodedCommitmentListBytes) {
		fmt.Println(
			"something went wrong when re-encoding the second commitment list, which should yield the same output",
		)
	}

}
Output:

Example (Existing_keys)

Example_existing_keys shows how to import existing keys in their canonical byte encoding.

package main

import (
	"encoding/hex"
	"fmt"
	"strings"

	"github.com/bytemare/frost"
)

func main() {
	ciphersuite := frost.Ristretto255
	id := 5
	signerSecretKey := "941c0685dc7c567dd206a39bce556008367fdf633b56c010cde5561435f75b0e"
	signerPublicKey := "d4b9a3acda8acb133c1eff7b99838908c3f9271569c734591ac8f609f321d01a"
	verificationKey := "4400e5808c12c6ef9dc751135acf76edfa73780c08e766537bb6c49bea591872"

	fmt.Println("Decoding to key share:")
	fmt.Printf("- signer identifier: %d\n", id)
	fmt.Printf("- signer secret key: %s\n", signerSecretKey)
	fmt.Printf("- signer public key: %s\n", signerPublicKey)
	fmt.Printf("- global verification key: %s\n", verificationKey)

	// First, let's rebuilt a public key share.
	signerPublicKeyBytes, err := hex.DecodeString(signerPublicKey)
	if err != nil {
		fmt.Println(err)
	}

	signerPublicKeyShare, err := frost.NewPublicKeyShare(ciphersuite, uint16(id), signerPublicKeyBytes)
	if err != nil {
		fmt.Println(err)
	}

	encodedPublicKeyShare := hex.EncodeToString(signerPublicKeyShare.Encode())
	fmt.Printf(
		"Decoded individual elements to a public key share, and re-encoded as a whole: %s\n",
		encodedPublicKeyShare,
	)

	// Now, we rebuilt a private key share.
	signerSecretKeyBytes, err := hex.DecodeString(signerSecretKey)
	if err != nil {
		fmt.Println(err)
	}

	verificationKeyBytes, err := hex.DecodeString(verificationKey)
	if err != nil {
		fmt.Println(err)
	}

	signerKeyShare, err := frost.NewKeyShare(
		ciphersuite,
		uint16(id),
		signerSecretKeyBytes,
		signerPublicKeyBytes,
		verificationKeyBytes,
	)
	if err != nil {
		fmt.Println(err)
	}

	encodedKeyShare := hex.EncodeToString(signerKeyShare.Encode())
	fmt.Printf("Decoded individual elements to a secret key share, and re-encoded as a whole: %s\n", encodedKeyShare)

	if !strings.HasPrefix(encodedKeyShare, encodedPublicKeyShare) {
		fmt.Println(
			"Something went wrong when re-encoding: the public key share must be part of the private key share.",
		)
	}

}
Output:

Decoding to key share:
- signer identifier: 5
- signer secret key: 941c0685dc7c567dd206a39bce556008367fdf633b56c010cde5561435f75b0e
- signer public key: d4b9a3acda8acb133c1eff7b99838908c3f9271569c734591ac8f609f321d01a
- global verification key: 4400e5808c12c6ef9dc751135acf76edfa73780c08e766537bb6c49bea591872
Decoded individual elements to a public key share, and re-encoded as a whole: 01050000000000d4b9a3acda8acb133c1eff7b99838908c3f9271569c734591ac8f609f321d01a
Decoded individual elements to a secret key share, and re-encoded as a whole: 01050000000000d4b9a3acda8acb133c1eff7b99838908c3f9271569c734591ac8f609f321d01a941c0685dc7c567dd206a39bce556008367fdf633b56c010cde5561435f75b0e4400e5808c12c6ef9dc751135acf76edfa73780c08e766537bb6c49bea591872
Example (Key_deserialization)

Example_key_deserialization shows how to encode and decode scalars (e.g. secret keys) and elements (e.g. public keys). Note you must know the group beforehand.

package main

import (
	"bytes"
	"encoding/hex"
	"fmt"

	"github.com/bytemare/frost"
)

func main() {
	ciphersuite := frost.Ristretto255
	group := ciphersuite.Group()

	// Private keys and scalars.
	privateKeyHex := "941c0685dc7c567dd206a39bce556008367fdf633b56c010cde5561435f75b0e"
	privateKey := group.NewScalar()

	// You can directly decode a hex string to a scalar.
	if err := privateKey.DecodeHex(privateKeyHex); err != nil {
		fmt.Println(err)
	}

	// Or you can use byte slices.
	privateKeyBytes, err := hex.DecodeString(privateKeyHex)
	if err != nil {
		fmt.Println(err)
	}

	if err = privateKey.Decode(privateKeyBytes); err != nil {
		fmt.Println(err)
	}

	if privateKeyHex != privateKey.Hex() {
		fmt.Println("something went wrong re-encoding the scalar in hex, which should yield the same output")
	}

	if !bytes.Equal(privateKeyBytes, privateKey.Encode()) {
		fmt.Println("something went wrong re-encoding the scalar in bytes, which should yield the same output")
	}

	// Same thing for public keys and group elements.
	publicKeyHex := "d4b9a3acda8acb133c1eff7b99838908c3f9271569c734591ac8f609f321d01a"
	publicKey := group.NewElement()

	// You can directly decode a hex string to an element.
	if err = publicKey.DecodeHex(publicKeyHex); err != nil {
		panic(err)
	}

	// Or you can use byte slices.
	publicKeyBytes, err := hex.DecodeString(publicKeyHex)
	if err != nil {
		panic(err)
	}

	if err = publicKey.Decode(publicKeyBytes); err != nil {
		panic(err)
	}

	if publicKeyHex != publicKey.Hex() {
		fmt.Println("something went wrong re-encoding the element in hex, which should yield the same output")
	}

	if !bytes.Equal(publicKeyBytes, publicKey.Encode()) {
		fmt.Println("something went wrong re-encoding the element in bytes, which should yield the same output")
	}

}
Output:

Example (Key_generation_centralised_trusted_dealer)

Example_key_generation shows how to create keys in a threshold setup with a centralized trusted dealer. - a decentralised protocol described in the original FROST paper

package main

import (
	"fmt"

	"github.com/bytemare/ecc"

	"github.com/bytemare/frost"
	"github.com/bytemare/frost/debug"
)

func main() {
	maxSigners := uint16(5)
	threshold := uint16(3)
	ciphersuite := frost.Default

	optionnalSecretKey := ciphersuite.Group().NewScalar().Random()
	keyShares, verificationKey, vssCommitment := debug.TrustedDealerKeygen(
		ciphersuite,
		optionnalSecretKey,
		threshold,
		maxSigners,
	)

	fmt.Printf("Created %d key shares with %d vss commitments and %d verification key.",
		len(keyShares),
		len(vssCommitment),
		len([]*ecc.Element{verificationKey}), // yes that line is ugly but it's pretext to use the variable produced.
	)

}
Output:

Created 5 key shares with 3 vss commitments and 1 verification key.
Example (Key_generation_decentralised)

Example_key_generation shows how to create keys in a threshold setup with distributed key generation described in the original FROST paper.

package main

import (
	"fmt"
)

func main() {
	fmt.Println("Visit github.com/bytemare/dkg for an example and documentation.")
}
Output:

Visit github.com/bytemare/dkg for an example and documentation.
Example (Signer)

Example_signer shows the execution steps of a FROST participant.

package main

import (
	"fmt"

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

	"github.com/bytemare/frost"
	"github.com/bytemare/frost/debug"
)

func main() {
	maxSigners := uint16(5)
	threshold := uint16(3)
	message := []byte("example message")
	ciphersuite := frost.Default

	// We assume you already have a pool of participants with distinct non-zero identifiers in [1:maxSingers]
	// and their signing share.
	// This example uses a centralised trusted dealer, but it is strongly recommended to use distributed key generation,
	// e.g. from github.com/bytemare/dkg, which is compatible with FROST.
	secretKeyShares, verificationKey, _ := debug.TrustedDealerKeygen(ciphersuite, nil, threshold, maxSigners)

	// Since we used a centralised key generation, we only take the first key share for our participant.
	participantSecretKeyShare := secretKeyShares[0]

	// At key generation, each participant must send their public key share to the coordinator, and the collection must
	// be broadcast to every participant.
	publicKeyShares := make([]*keys.PublicKeyShare, len(secretKeyShares))
	for i, sk := range secretKeyShares {
		publicKeyShares[i] = sk.Public()
	}

	// This is how to set up the Configuration for FROST, the same for every signer and the coordinator.
	// Note that every configuration setup for a Signer needs the public key shares of all other signers participating
	// in a signing session (at least for the Sign() step).
	configuration := &frost.Configuration{
		Ciphersuite:           ciphersuite,
		Threshold:             threshold,
		MaxSigners:            maxSigners,
		VerificationKey:       verificationKey,
		SignerPublicKeyShares: publicKeyShares,
	}

	if err := configuration.Init(); err != nil {
		panic(err)
	}

	// Instantiate the participant using its secret share.
	// A participant (or Signer) can be backed up by serialization, and directly instantiated from that backup.
	participant, err := configuration.Signer(participantSecretKeyShare)
	if err != nil {
		panic(err)
	}

	// Step 1: call Commit() on each participant. This will return the participant's single-use commitment for a
	// signature (which is independent of the future message to sign).
	// Send this to the coordinator or all other participants (depending on your setup) over an authenticated
	// channel (confidentiality is not required).
	// A participant (or Signer) keeps an internal state during the protocol run across the two rounds.
	// A participant can pre-compute multiple commitments in advance: these commitments can be shared, but the
	// participant keeps an internal state of corresponding values, so it must the same instance or a backup of it using
	// the serialization functions.
	com := participant.Commit()

	// Step 2: collect the commitments from the other participants and coordinator-chosen message to sign,
	// and finalize by signing the message.
	commitments := make(frost.CommitmentList, threshold)
	commitments[0] = com

	// This is not part of a participant's flow, but we need to collect the commitments of the other participants for
	// the demo.
	{
		for i := uint16(1); i < threshold; i++ {
			signer, err := configuration.Signer(secretKeyShares[i])
			if err != nil {
				panic(err)
			}

			commitments[i] = signer.Commit()

		}
	}

	// Step 3: The participant receives the commitments from the other signers and the message to sign.
	// Sign produces a signature share to be sent back to the coordinator.
	// Execution MUST be aborted upon errors.
	signatureShare, err := participant.Sign(message, commitments)
	if err != nil {
		panic(err)
	}

	// This shows how to verify a single signature share
	if err = configuration.VerifySignatureShare(signatureShare, message, commitments); err != nil {
		panic(fmt.Sprintf("signature share verification failed: %s", err))
	}

	fmt.Println("Signing successful.")

}
Output:

Signing successful.

Index

Examples

Constants

View Source
const (
	// Default and recommended ciphersuite for FROST.
	Default = Ristretto255

	// Ristretto255 uses Ristretto255 and SHA-512. This ciphersuite is recommended.
	Ristretto255 = Ciphersuite(ecc.Ristretto255Sha512)

	// P256 uses P-256 and SHA-256.
	P256 = Ciphersuite(ecc.P256Sha256)

	// P384 uses P-384 and SHA-384.
	P384 = Ciphersuite(ecc.P384Sha384)

	// P521 uses P-521 and SHA-512.
	P521 = Ciphersuite(ecc.P521Sha512)

	// Ed25519 uses Edwards25519 and SHA-512, producing Ed25519-compliant signatures as specified in RFC8032.
	Ed25519 = Ciphersuite(ecc.Edwards25519Sha512)

	// Secp256k1 uses Secp256k1 and SHA-256.
	Secp256k1 = Ciphersuite(ecc.Secp256k1Sha256)
)

Variables

This section is empty.

Functions

func NewKeyShare

func NewKeyShare(
	c Ciphersuite,
	id uint16,
	secretShare, signerPublicKey, verificationKey []byte,
) (*keys.KeyShare, error)

NewKeyShare returns a KeyShare from separately encoded key material. To deserialize a byte string produced by the KeyShare.Encode() method, use the KeyShare.Decode() method.

func NewPublicKeyShare

func NewPublicKeyShare(c Ciphersuite, id uint16, signerPublicKey []byte) (*keys.PublicKeyShare, error)

NewPublicKeyShare returns a PublicKeyShare from separately encoded key material. To deserialize a byte string produced by the PublicKeyShare.Encode() method, use the PublicKeyShare.Decode() method.

func SchnorrChallenge

func SchnorrChallenge(g ecc.Group, msg []byte, r, pk *ecc.Element) *ecc.Scalar

SchnorrChallenge computes the per-message SchnorrChallenge.

func VerifySignature

func VerifySignature(c Ciphersuite, message []byte, signature *Signature, publicKey *ecc.Element) error

VerifySignature returns whether the signature of the message is valid under publicKey.

Types

type BindingFactors

type BindingFactors map[uint16]*ecc.Scalar

BindingFactors is a map of participant identifiers to BindingFactors.

type Ciphersuite

type Ciphersuite byte

Ciphersuite identifies the group and hash function to use for FROST.

func (Ciphersuite) Available

func (c Ciphersuite) Available() bool

Available returns whether the selected ciphersuite is available.

func (Ciphersuite) Group

func (c Ciphersuite) Group() ecc.Group

Group returns the elliptic curve group used in the ciphersuite.

type Commitment

type Commitment struct {
	HidingNonceCommitment  *ecc.Element `json:"hidingNonceCommitment"`
	BindingNonceCommitment *ecc.Element `json:"bindingNonceCommitment"`
	CommitmentID           uint64       `json:"commitmentId"`
	SignerID               uint16       `json:"signerId"`
	Group                  ecc.Group    `json:"group"`
}

Commitment is a participant's one-time commitment holding its identifier, and hiding and binding nonces.

func (*Commitment) Copy

func (c *Commitment) Copy() *Commitment

Copy returns a new Commitment struct populated with the same values as the receiver.

func (*Commitment) Decode

func (c *Commitment) Decode(data []byte) error

Decode attempts to deserialize the encoded commitment given as input, and to return it.

func (*Commitment) DecodeHex

func (c *Commitment) DecodeHex(h string) error

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

func (*Commitment) Encode

func (c *Commitment) Encode() []byte

Encode returns the serialized byte encoding of a participant's commitment.

func (*Commitment) Hex

func (c *Commitment) Hex() string

Hex returns the hexadecimal representation of the byte encoding returned by Encode().

func (*Commitment) UnmarshalJSON

func (c *Commitment) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes data into c, or returns an error.

type CommitmentList

type CommitmentList []*Commitment

CommitmentList is a sortable list of commitments with search functions.

func DecodeList

func DecodeList(data []byte) (CommitmentList, error)

DecodeList decodes a byte string produced by the CommitmentList.Encode() method.

func (CommitmentList) Encode

func (c CommitmentList) Encode() []byte

Encode serializes the CommitmentList into a compact byte encoding.

func (CommitmentList) Get

func (c CommitmentList) Get(identifier uint16) *Commitment

Get returns the commitment of the participant with the corresponding identifier, or nil if it was not found.

func (CommitmentList) IsSorted

func (c CommitmentList) IsSorted() bool

IsSorted returns whether the list is sorted in ascending order by identifier.

func (CommitmentList) Participants

func (c CommitmentList) Participants() []uint16

Participants returns the uint64 list of participant identifiers in the list.

func (CommitmentList) ParticipantsScalar

func (c CommitmentList) ParticipantsScalar() []*ecc.Scalar

ParticipantsScalar returns the ecc.Scalar list of participant identifier in the list.

func (CommitmentList) Sort

func (c CommitmentList) Sort()

Sort sorts the list the ascending order of identifiers.

type Configuration

type Configuration struct {
	VerificationKey       *ecc.Element           `json:"verificationKey"`
	SignerPublicKeyShares []*keys.PublicKeyShare `json:"signerPublicKeyShares"`
	Threshold             uint16                 `json:"threshold"`
	MaxSigners            uint16                 `json:"maxSigners"`
	Ciphersuite           Ciphersuite            `json:"ciphersuite"`
	// contains filtered or unexported fields
}

Configuration holds the Configuration for a signing session.

func (*Configuration) AggregateSignatures

func (c *Configuration) AggregateSignatures(
	message []byte,
	sigShares []*SignatureShare,
	commitments CommitmentList,
	verify bool,
) (*Signature, error)

AggregateSignatures enables a coordinator to produce the final signature given all signature shares.

Before aggregation, each signature share must be a valid, deserialized element. If that validation fails the coordinator must abort the protocol, as the resulting signature will be invalid. The CommitmentList must be sorted in ascending order by identifier.

The coordinator should verify this signature using the group public key before publishing or releasing the signature. This aggregate signature will verify if and only if all signature shares are valid. If an invalid share is identified a reasonable approach is to remove the signer from the set of allowed participants in future runs of FROST. If verify is set to true, AggregateSignatures will automatically verify the signature shares, and will return an error on the first encountered invalid signature share.

func (*Configuration) Decode

func (c *Configuration) Decode(data []byte) error

Decode deserializes the input data into the Configuration, or returns an error.

func (*Configuration) DecodeHex

func (c *Configuration) DecodeHex(h string) error

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

func (*Configuration) Encode

func (c *Configuration) Encode() []byte

Encode serializes the Configuration into a compact byte slice.

func (*Configuration) Hex

func (c *Configuration) Hex() string

Hex returns the hexadecimal representation of the byte encoding returned by Encode().

func (*Configuration) Init

func (c *Configuration) Init() error

Init verifies whether the configuration's components are valid, in which case it initializes internal values, or returns an error otherwise.

func (*Configuration) Signer

func (c *Configuration) Signer(keyShare *keys.KeyShare) (*Signer, error)

Signer returns a new participant of the protocol instantiated from the Configuration and the signer's key share.

func (*Configuration) UnmarshalJSON

func (c *Configuration) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes data into c, or returns an error.

func (*Configuration) ValidateCommitment

func (c *Configuration) ValidateCommitment(commitment *Commitment) error

ValidateCommitment returns an error if the commitment is not valid.

func (*Configuration) ValidateCommitmentList

func (c *Configuration) ValidateCommitmentList(commitments CommitmentList) error

ValidateCommitmentList returns an error if at least one of the following conditions is not met: - list length is within [threshold;max]. - no signer identifier in commitments is 0. - no singer identifier in commitments is > max signers. - no duplicated in signer identifiers. - all commitment signer identifiers are registered in the configuration.

func (*Configuration) ValidateKeyShare

func (c *Configuration) ValidateKeyShare(keyShare *keys.KeyShare) error

ValidateKeyShare returns an error if they KeyShare has invalid components or properties that not compatible with the configuration.

func (*Configuration) ValidatePublicKeyShare

func (c *Configuration) ValidatePublicKeyShare(pks *keys.PublicKeyShare) error

ValidatePublicKeyShare returns an error if they PublicKeyShare has invalid components or properties that not compatible with the configuration.

func (*Configuration) VerifySignatureShare

func (c *Configuration) VerifySignatureShare(
	sigShare *SignatureShare,
	message []byte,
	commitments CommitmentList,
) error

VerifySignatureShare verifies a signature share. sigShare is the signer's signature share to be verified.

The CommitmentList must be sorted in ascending order by identifier.

type Nonce

type Nonce struct {
	HidingNonce  *ecc.Scalar `json:"hidingNonce"`
	BindingNonce *ecc.Scalar `json:"bindingNonce"`
	*Commitment  `json:"commitment"`
}

Nonce holds the signing nonces and their commitments. The Signer.Commit() method will generate and record a new nonce and return the Commitment to that nonce. That Commitment will be used in Signer.Sign() and the associated nonces to create a signature share. Note that nonces and their commitments are agnostic of the upcoming message to sign, and can therefore be pre-computed and the commitments shared before the signing session, saving a round-trip.

func (*Nonce) UnmarshalJSON

func (n *Nonce) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes data into n, or returns 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 (*Signature) Clear

func (s *Signature) Clear()

Clear overwrites the original values with default ones.

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.

func (*Signature) Hex

func (s *Signature) Hex() string

Hex returns the hexadecimal representation of the byte encoding returned by Encode().

func (*Signature) UnmarshalJSON

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

UnmarshalJSON decodes data into s, or returns an error.

type SignatureShare

type SignatureShare struct {
	SignatureShare   *ecc.Scalar `json:"signatureShare"`
	SignerIdentifier uint16      `json:"signerIdentifier"`
	Group            ecc.Group   `json:"group"`
}

SignatureShare represents a Signer's signature share and its identifier.

func (*SignatureShare) Decode

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

Decode takes a byte string and attempts to decode it to return the signature share.

func (*SignatureShare) DecodeHex

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

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

func (*SignatureShare) Encode

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

Encode returns a compact byte encoding of the signature share.

func (*SignatureShare) Hex

func (s *SignatureShare) Hex() string

Hex returns the hexadecimal representation of the byte encoding returned by Encode().

func (*SignatureShare) UnmarshalJSON

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

UnmarshalJSON decodes data into s, or returns an error.

type Signer

type Signer struct {
	// The KeyShare holds the signer's secret and public info, such as keys and identifier.
	KeyShare *keys.KeyShare `json:"keyShare"`

	// LambdaRegistry records all interpolating values for the signers for different combinations of participant
	// groups. Each group makes up a unique polynomial defined by the participants' identifiers. A value will be
	// computed once for the first time a group is encountered, and kept across encodings and decodings of the signer,
	// accelerating subsequent signatures within the same group of signers.
	LambdaRegistry internal.LambdaRegistry `json:"lambdaRegistry"`

	// NonceCommitments maps Nonce and their NonceCommitments to their Commitment's identifier.
	NonceCommitments map[uint64]*Nonce `json:"nonceCommitments"`

	// Configuration is the core FROST setup configuration.
	Configuration *Configuration `json:"configuration"`

	// HidingRandom can be set to force the use its value for HidingNonce generation. This is only encouraged for vector
	// reproduction, but should be left to nil in any production deployments.
	HidingRandom []byte `json:"hidingRandom,omitempty"`

	// HidingRandom can be set to force the use its value for HidingNonce generation. This is only encouraged for vector
	// reproduction, but should be left to nil in any production deployments.
	BindingRandom []byte `json:"bindingRandom,omitempty"`
}

Signer is a participant in a signing group.

func (*Signer) ClearNonceCommitment

func (s *Signer) ClearNonceCommitment(commitmentID uint64)

ClearNonceCommitment zeroes-out the nonces and their commitments, and unregisters the nonce record.

func (*Signer) Commit

func (s *Signer) Commit() *Commitment

Commit generates a signer's nonces and commitment, to be used in the second FROST round. The internal nonce must be kept secret, and the returned commitment sent to the signature aggregator.

func (*Signer) Decode

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

Decode attempts to deserialize the encoded backup data into the Signer.

func (*Signer) DecodeHex

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

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

func (*Signer) Encode

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

Encode serializes the client with its long term values, containing its secret share. This is useful for saving state and backup.

func (*Signer) Hex

func (s *Signer) Hex() string

Hex returns the hexadecimal representation of the byte encoding returned by Encode().

func (*Signer) Identifier

func (s *Signer) Identifier() uint16

Identifier returns the Signer's identifier.

func (*Signer) Sign

func (s *Signer) Sign(message []byte, commitments CommitmentList) (*SignatureShare, error)

Sign produces a participant's signature share of the message msg. The CommitmentList must contain a Commitment produced on a previous call to Commit(). Once the signature share with Sign() is produced, the internal commitment and nonces are cleared and another call to Sign() with the same Commitment will return an error.

func (*Signer) UnmarshalJSON

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

UnmarshalJSON decodes data into s, or returns an error.

func (*Signer) VerifyCommitmentList

func (s *Signer) VerifyCommitmentList(commitments CommitmentList) error

VerifyCommitmentList checks for the Commitment list integrity and the signer's commitment. This function must not return an error for Sign to succeed.

Directories

Path Synopsis
Package debug provides tools for key generation and verification for debugging purposes.
Package debug provides tools for key generation and verification for debugging purposes.
Package internal provides values, structures, and functions to operate FROST that are not part of the public API.
Package internal provides values, structures, and functions to operate FROST that are not part of the public API.

Jump to

Keyboard shortcuts

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