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.
- 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.
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 examples and comments can be found in examples_test.go.
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.
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).
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.
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.
Once the configuration is initialised, setting up a signer is straightforward, using the Signer()
method
and providing the signer's KeyShare
.
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.
- 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.
- 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)
.
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.
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.
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
You can find the godoc documentation and usage examples in the package doc.
SemVer is used for versioning. For the versions available, see the tags on the repository.
Please read CONTRIBUTING.md for details on the code of conduct, and the process for submitting pull requests.
This project is licensed under the MIT License - see the LICENSE file for details.