Signing

The diagram below shows the signing process. Dashed lines represent data being sent through an authenticated communication channel.

Diagram of Signing, illustrating what is explained in the text

Coordinator, Round 1

To sign, the Coordinator must select which participants are going to generate the signature, and must signal to start the process. This needs to be implemented by users of the ZF FROST library and will depend on the communication channel being used.

Participants, Round 1

Each selected participant will then generate the nonces (a SigningNonces) and their commitments (a SigningCommitments) by calling round1::commit():

    let (nonces, commitments) = frost::round1::commit(
        key_packages[&participant_identifier].signing_share(),
        &mut rng,
    );

The SigningNonces must be kept by the participant to use in Round 2, while the SigningCommitments must be sent to the Coordinator using an authenticated channel.

Coordinator, Round 2

The Coordinator will get all SigningCommitments from the participants and the message to be signed, and then build a SigningPackage by calling SigningPackage::new().

let message = "message to sign".as_bytes();
// In practice, the SigningPackage must be sent to all participants
// involved in the current signing (at least min_signers participants),
// using an authenticate channel (and confidential if the message is secret).
let signing_package = frost::SigningPackage::new(commitments_map, message);

The SigningPackage must then be sent to all the participants using an authenticated channel. (Of course, if the message is confidential, then the channel must also be confidential.)

Warning

In all of the main FROST ciphersuites, the entire message must be sent to participants. In some cases, where the message is too big, it may be necessary to send a hash of the message instead. We strongly suggest creating a specific ciphersuite for this, and not just sending the hash as if it were the message. For reference, see how RFC 8032 handles "pre-hashing".

Participants, Round 2

Upon receiving the SigningPackage, each participant will then produce their signature share using their KeyPackage from the key generation process and their SigningNonces from Round 1, by calling round2::sign():

    let signature_share = frost::round2::sign(&signing_package, nonces, key_package)?;

The resulting SignatureShare must then be sent back to the Coordinator using an authenticated channel.

Important

In most applications, it is important that the participant must be aware of what they are signing. Thus the application should show the message to the participant and obtain their consent to proceed before producing the signature share.

Coordinator, Aggregate

Upon receiving the SignatureShares from the participants, the Coordinator can finally produce the final signature by calling aggregate() with the same SigningPackage sent to the participants and the PublicKeyPackage from the key generation (which is used to validate each SignatureShare).

let group_signature = frost::aggregate(&signing_package, &signature_shares, &pubkey_package)?;

The returned signature, a Signature, will be a valid signature for the message in the SigningPackage in Round 2 for the group verifying key in the PublicKeyPackage.

Note

FROST supports identifiable abort: if a participant misbehaves and produces an invalid signature share, then aggregation will fail and the returned error will have the identifier of the misbehaving participant. (If multiple participants misbehave, only the first one detected will be returned.)

What should be done in that case is up to the application. The misbehaving participant could be excluded from future signing sessions, for example.

Verifying signatures

The Coordinator could verify the signature with:

let is_signature_valid = pubkey_package
    .verifying_key()
    .verify(message, &group_signature)
    .is_ok();

(There is no need for the Coordinator to verify the signature since that already happens inside aggregate(). This just shows how the signature can be verified.)