/frost-ed25519

Implementation of the FROST protocol for threshold Ed25519 signing

Primary LanguageGoApache License 2.0Apache-2.0

FROST-Ed25519

License

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 curve
  • G 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 a eddsa.SecretShare struct
  • a set of all public shares {A_i} stored in eddsa.Public struct
  • the group key A represented as a eddsa.PublicKey, and stored in the GroupKey field of eddsa.Public. Calling PublicKey.ToEd25519() returns an ed25519.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 as R = [r]•G for some r mod q.
  • 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.

Compatibility with ed25519:

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.IDs 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 States 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 to nil, 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.