FROST-Ed25519
A Go implementation of a FROST threshold signature protocol for the Ed25519 signature scheme.
Our FROST protocol implementation is also inspired from that in the IETF Draft Threshold Modes in Elliptic Curves .
Ed25519
Ed25519 is an instance of the EdDSA construction, defined over the Edwards 25519 elliptic curve. FROST-Ed25519 is compatible with Ed25519, in the sense that public keys follow the same prescribed format, and that the same verification algorithm can be used.
Specifically, we implement the PureEdDSA variant, as detailed in RFC 8032 (as opposed to HashEdDSA/Ed25519ph or ContextEdDSA/Ed25519ctx.).
Ristretto
In order to minimize the impact of the cofactor in the Edwards 25519 elliptic curve, we represent the curve points with the Ristretto encoding. Our implementation is taken from Filippo Valsorda's branch of George Tankersley's ristretto255. Internally it uses the edwards25519 package.
We add a BytesEd25519()
method on group elements which allows us to recover an Ed25519 compatible encoding of the curve point.
As elements are represented internally by edwards25519.Point
, we take this point P
and remove the cofactor by computing P' = [8^{-1}][8]P
.
The result is the canonical encoding of P'
.
For clarity, we distinguish the following elements:
B
is the base point of the Edwards 25519 elliptic curveG
is the generator of the Ristretto group
The integer q
is equal to 2**252 + 27742317777372353535851937790883648493
and is the prime order of the Ristretto group <G>
.
Keys
The Ed25519 standard defines the private signing key as a 32 byte seed x
.
Taking the SHA-512 hash of x
yields two 32 byte strings by splitting the output in two equal halves: SHA-512(x) = s || prefix
.
The value s
encodes 32 byte integer, while the prefix
is used later for deterministic signature generation.
The public key is the canonical representation of the elliptic curve point A = [s mod q] • B
.
In FROST-Ed25519, a group of n
parties P1, ..., Pn
each hold a Shamir share s_i
of the secret integer s
.
These shares are represented as integers mod q
.
Given any set of at least t+1
distinct shares, it is possible to recover the original full secret key s mod q
.
The integer t
is the threshold of the scheme, and defines the maximum number of parties that could act maliciously (i.e. collaborate to recover the key).
In FROST-Ed25519, the parties obtain their shares of s
by executing a Distributed Key Generation (DKG) protocol.
In addition to receiving individual shares s_i
, all parties also obtain the group key A = [s]•G
, and its associated public shares {A_i = [s_i]•G}
.
After a successful execution of the DKG protocol, each party Pi
obtains:
- a secret share
s_i
represented as aeddsa.SecretShare
struct - a set of all public shares
{A_i}
stored ineddsa.Public
struct - the group key
A
represented as aeddsa.PublicKey
, and stored in theGroupKey
field ofeddsa.Public
. CallingPublicKey.ToEd25519()
returns aned25519.PublicKey
compatible with the Ed25519 standard.
Signatures
A FROST-Ed25519 signature for a message M
is defined by a pair (R,S)
where:
- The nonce
R
represents a Ristretto group element, computed asR = [r]•G
for somer
modq
. S
is a scalar derived computed as
R = [r]•G
k = SHA-512(R.BytesEd25519() || A.BytesEd25519() || M)
S = (r + k * s) mod q
In the original Ed25519 scheme, the nonce R = [r]•B
is generated deterministically using the prefix
in the key generation,
and the integer r
is computed as r = H( prefix || M )
.
For threshold signing, it is harder to generate nonce in such a deterministic way.
In FROST-Ed25519, the nonce pair (r,R)
is generated as detailed in the FROST paper
For compatibility with Ed25519, k
is computed by encoding R
and A
as their canonical representations in the edwards25519 curve (cofactor-less).
Signatures are represented by the eddsa.Signature
type.
Verification
The verification algorithm takes a public key A
, the signed message M
, and its signature (R,S)
.
It does the following:
- Recompute
k = SHA-512(R.BytesEd25519() || A.BytesEd25519() || M) mod q
- Verify the equality
R == [-k]•A + [S]•G
Manual verification is not necessary in most cases, but is possible by calling PublicKey.Verify(message []byte, signature *eddsa.Signature)
.
Note: the cofactor is no longer an issue here, since we are considering points in the Ristretto group.
ed25519
:
Compatibility with The goal of FROST-Ed25519 is to be compatible with the ed25519
library included in Go.
In particular, the frost.PublicKey
and frost.Signature
types can be converted to the ed25119.PublicKey
and []byte
types respectively,
by calling .ToEd25519()
.
Example
The following example shows some possible interaction with the types described above:
var (
id party.ID // id of the party
secretShare *eddsa.SecretShare // private output of DKG for party id
public *eddsa.Public // public output of DKG
message []byte // message signed
groupSig *eddsa.Signature // signature produced by sign protocol for message
)
groupKey := public.GroupKey
// use the ed25519 library
ed25519.Verify(groupKey.ToEd25519(), message, groupSig.ToEd25519()) // = true
secretShare.ID == id // = true
Protocol version implemented
The FROST paper proposes two variants of the protocol. We implement the "single-round" version of FROST, rather than the 4-round variant FROST-Interactive.
The single-round version does one "offline" round, followed by one "online" round, where the offline round does not need the message and can therefore be precomputed. For simplicity, we group both steps together and achieve a 2 round protocol that requires less state handling. We also ignore the role of signature aggregator and instead let the parties broadcast the signature shares to each other to obtain the full signature.
This variant is the one that is proposed for practical implementations, however it does not have a full security proof, unlike FROST-Interactive (see Section 6.2 of the FROST paper).
Instructions
This FROST-Ed25519 implementation includes a round-based architecture for both the key generation and signing protocols.
The cryptographic protocols are defined in pkg/frost/keygen and pkg/frost/sign.
They are handled by a State
object that takes care of storing messages, passing them to the round at the right time, and reporting any error that may have occurred.
Users of this library should only interact with State
types.
Basics
Each party must be assigned a unique numerical party.ID
(internally represented as an uint16
).
A set of party.ID
s is stored as a party.IDSlice
which wraps a slice and ensures sorting.
Optionally, a timeout
argument can be provided, to force the protocol to abort if the time duration between two received messages is longer than timeout
.
If it is set to 0, then there is no limit.
Appropriate State
s can be created by calling the functions frost.NewKeygenState
or frost.NewSignState
.
They both return the following:
- A
State
object used to interact with the protocol - An
Output
object whose attributes are initialized tonil
, and populated asynchronously when protocol has successfully completed. - An
error
indicating whether the state was successfully created.
An example of how to use the State
struct can be found in example/main.go.
Keygen
The key generation protocol we implement is as described in the original paper.
Calling frost.NewKeygenState
with the following arguments creates a State
object that can execute the protocol.
var (
partyID party.ID // ID of the party initiating the key generation (`ID` type is an alias for `uint16`)
partyIDs party.IDSlice // sorted slice of all party IDs
threshold party.Size // maximum number of corrupted parties allowed (`threshold`+1 parties required for signing)
timeout time.Duration // maximum time allowed between two messages received. A duration of 0 indicates no timeout
)
state, output, err := frost.NewKeygenState(partyID, partyIDs, threshold, timeout)
Once the protocol has finished, the output
contains the following two fields:
Public
contains the public key shares of all parties that participated in the protocol, as well as the group key these define.SecretKey
is the party's share of the group's signing key.
Sign
var (
partyIDs party.IDSlice // slice of party IDs which will be performing the signing (must be of length at least `threshold`+1)
secret *eddsa.SecretShare // the secret key share obtained from the keygen protocol
public *eddsa.Public // contains the public information including the group key and individual public shares
message []byte // message in bytes to be signed (does not need to be prehashed)
timeout time.Duration // maximum time allowed between two messages received. A duration of 0 indicates no timeout
)
state, output, err := frost.NewSignState(partySet, secret, public, message, timeout)
Once the protocol has finished, the output
contains a single field for the Signature
:
The Signature can be verified using Go's included ed25519
library, by converting the group key and signature to compatible types.
ed25519.Verify(shares.GroupKey.ToEd25519(), message, output.Signature.ToEd25519())
or alternatively,
Transport Layer
If the round was successfully executed, State.ProcessAll()
returns a slice []*messages.Message
.
It is up to the user of this library to properly route messages between participants.
The ID's of the sender and destination party of a particular messages.Message
can be found in the From
and To
field of the embedded messages.Header
on the messages.Message
object.
Users should first check if the message is intended for broadcast by calling .IsBroadcast()
, since the To
field is undefined in this case.
var msg messages.Message
data, err := msg.MarshalBinary()
if err != nil {
// handle marshalling error, but we cannot continue
return
}
if msg.IsBroadcast() {
// send data to all parties except ourselves
} else {
dest := msg.To
// send data to party with ID dest
}
On the reception, the message should be unmarshalled and then given to the State
:
var data []byte
var msg messages.Message
err := msg.Unmarshal(data)
if err != nil {
// handle marshalling error, but we cannot continue
return
}
err = state.HandleMessage(&msg)
if err != nil {
// May indicate that an error occurred during transport
// does not mean we should abort necessarily
return
}
Testing
We include unit tests for individual modules, as well as a bigger integration tests in test/. Full test coverage is however not guaranteed.
Example usage
A simple example of how to use this library can be found in test/sign_test.go and test/keygen_test.go.
Security
This library was NOT designed to be free of side channels (timing, memory, oracles, and so on), and due to Go's intrinsic limitations most likely is not.
This library has yet to be audited and fully vetted for production usage. Use at your own risk.
Please report any critical security issue to security@taurusgroup.ch. We encourage you to use our PGP key:
-----BEGIN PGP PUBLIC KEY BLOCK-----
mDMEX3G3ARYJKwYBBAHaRw8BAQdA7sQCSqSkAmGylsLRJepXuAZKkcWA+EWRPeGa
22cIXYC0KVRhdXJ1cyBTZWN1cml0eSA8c2VjdXJpdHlAdGF1cnVzZ3JvdXAuY2g+
iJAEExYIADgWIQQ0q1qzH0uLrdBgWQWfaUpuIE2KEAUCX3G3AQIbIwULCQgHAgYV
CgkICwIEFgIDAQIeAQIXgAAKCRCfaUpuIE2KEFn9AP9uAyItJevrH8rV3K4zO25X
7nOI8MQJagBMnGxP+FdF7QD8D3LndQy2AefifK44v8BOKHs0J/hXtkIJTFLu6IzG
MwA=
=QNKX
-----END PGP PUBLIC KEY BLOCK-----
Issues that are not critical (not exploitable, DoS, and so on) can be reported as GitHub Issues.
Dependencies
Our package has a minimal set of third-party dependencies, mainly Valsorda's edwards25519.
We also include the single ristretto255
file from PR 41
Intellectual property
This code is copyright (c) Taurus SA, 2021, and under Apache 2.0 license.