Signing
The diagram below shows the signing process. Dashed lines represent data being sent through an authenticated communication channel.
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_package.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.)
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.
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 SignatureShare
s 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
.
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.)