Module: FROST

Defined in:
lib/frost.rb,
lib/frost/dkg.rb,
lib/frost/hash.rb,
lib/frost/nonce.rb,
lib/frost/dealer.rb,
lib/frost/context.rb,
lib/frost/version.rb,
lib/frost/signature.rb,
lib/frost/polynomial.rb,
lib/frost/repairable.rb,
lib/frost/commitments.rb,
lib/frost/signing_key.rb,
lib/frost/secret_share.rb,
lib/frost/dkg/public_package.rb,
lib/frost/dkg/secret_package.rb

Defined Under Namespace

Modules: DKG, Hash, Repairable, Type Classes: Commitments, Context, Dealer, Error, Nonce, Polynomial, SecretShare, Signature, SigningKey

Constant Summary collapse

VERSION =
"0.5.0"

Class Method Summary collapse

Class Method Details

.aggregate(context, commitment_list, msg, group_pubkey, sig_shares) ⇒ FROST::Signature

Aggregates the signature shares to produce a final signature that can be verified with the group public key.

Parameters:

  • context (FROST::Context)

    FROST context.

  • commitment_list (Array)

    A list of commitments issued by each participant.

  • msg (String)

    The message to be signed.

  • group_pubkey (ECDSA::Point)

    Public key corresponding to the group signing key.

  • sig_shares (Array)

    A set of signature shares z_i, integer values.

Returns:

Raises:

  • (ArgumentError)


158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/frost.rb', line 158

def aggregate(context, commitment_list, msg, group_pubkey, sig_shares)
  raise ArgumentError "context must be FROST::Context." unless context.is_a?(FROST::Context)
  raise ArgumentError, "msg must be String." unless msg.is_a?(String)
  raise ArgumentError, "group_pubkey must be ECDSA::Point." unless group_pubkey.is_a?(ECDSA::Point)
  raise ArgumentError, "group_pubkey and context groups are different." unless context.group == group_pubkey.group
  raise ArgumentError, "The numbers of commitment_list and sig_shares do not match." unless commitment_list.length == sig_shares.length

  binding_factors = compute_binding_factors(context, group_pubkey, commitment_list, msg)
  group_commitment = compute_group_commitment(commitment_list, binding_factors)

  field = ECDSA::PrimeField.new(context.group.order)
  s = sig_shares.inject(0) do |sum, z_i|
    raise ArgumentError, "sig_shares must be array of integer" unless z_i.is_a?(Integer)
    field.mod(sum + z_i)
  end

  Signature.new(context, group_commitment, field.mod(s))
end

.compute_binding_factors(context, group_pubkey, commitment_list, msg) ⇒ Hash

Compute binding factors. www.ietf.org/archive/id/draft-irtf-cfrg-frost-15.html#name-binding-factors-computation This list must be sorted in ascending order by identifier.

Parameters:

  • context (FROST::Context)
  • group_pubkey (ECDSA::Point)
  • commitment_list (Array)

    The list of commitments issued by each participants.

  • msg (String)

    The message to be signed.

Returns:

  • (Hash)

    The hash of binding factor.

Raises:

  • (ArgumentError)


68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/frost.rb', line 68

def compute_binding_factors(context, group_pubkey, commitment_list, msg)
  raise ArgumentError "context must be FROST::Context." unless context.is_a?(FROST::Context)
  raise ArgumentError, "group_pubkey must be ECDSA::Point." unless group_pubkey.is_a?(ECDSA::Point)
  raise ArgumentError, "msg must be String." unless msg.is_a?(String)
  raise ArgumentError, "group_pubkey and context groups are different." unless context.group == group_pubkey.group

  msg_hash = Hash.h4(msg, context)
  encoded_commitment = Commitments.encode_group_commitment(commitment_list)
  encoded_commitment_hash = Hash.h5(encoded_commitment, context)
  rho_input_prefix = [group_pubkey.to_hex].pack("H*") + msg_hash + encoded_commitment_hash
  binding_factors = {}
  commitment_list.each do |commitments|
    preimage = rho_input_prefix + encode_identifier(commitments.identifier, context.group)
    binding_factors[commitments.identifier] = Hash.h1(preimage, context)
  end
  binding_factors
end

.compute_challenge(context, group_commitment, group_pubkey, msg) ⇒ Integer

Create the per-message challenge. If context type is BIP-340(taproot), only the X coordinate of R and group_pubkey are hashed, unlike vanilla FROST.

Parameters:

  • group_commitment (ECDSA::Point)

    The group commitment.

  • group_pubkey (ECDSA::Point)

    The public key corresponding to the group signing key.

  • msg (String)

    The message to be signed.

Returns:

  • (Integer)

    challenge

Raises:

  • (ArgumentError)


104
105
106
107
108
109
110
111
112
113
# File 'lib/frost.rb', line 104

def compute_challenge(context, group_commitment, group_pubkey, msg)
  raise ArgumentError "context must be FROST::Context." unless context.is_a?(FROST::Context)
  raise ArgumentError, "group_commitment must be ECDSA::Point." unless group_commitment.is_a?(ECDSA::Point)
  raise ArgumentError, "group_pubkey must be ECDSA::Point." unless group_pubkey.is_a?(ECDSA::Point)
  raise ArgumentError, "msg must be String." unless msg.is_a?(String)
  raise ArgumentError, "group_pubkey and context groups are different." unless context.group == group_pubkey.group

  preimage = context.normalize(group_commitment) + context.normalize(group_pubkey) + msg
  Hash.h2(preimage, context)
end

.compute_group_commitment(commitment_list, binding_factors) ⇒ ECDSA::Point

Compute the group commitment

Parameters:

  • commitment_list (Array)

    The list of commitments.

  • binding_factors (Hash)

    The map of binding factors.

Returns:

  • (ECDSA::Point)


90
91
92
93
94
95
96
# File 'lib/frost.rb', line 90

def compute_group_commitment(commitment_list, binding_factors)
  commitment_list.inject(commitment_list.first.hiding.group.infinity) do |sum, commitments|
    binding_factor = binding_factors[commitments.identifier]
    binding_nonce = commitments.binding * binding_factor
    binding_nonce + commitments.hiding + sum
  end
end

.encode_identifier(identifier, group) ⇒ String

Encode identifier

Parameters:

  • identifier (Integer)
  • group (ECDSA::Group)

Returns:

  • (String)

    The encoded identifier



51
52
53
54
55
56
57
58
# File 'lib/frost.rb', line 51

def encode_identifier(identifier, group)
  case group
  when ECDSA::Group::Secp256k1, ECDSA::Group::Secp256r1
    ECDSA::Format::IntegerOctetString.encode(identifier, 32)
  else
    raise RuntimeError, "group #{group} dose not supported."
  end
end

.sign(context, secret_share, group_pubkey, nonces, msg, commitment_list) ⇒ Integer

Generate signature share.

Parameters:

  • context (FROST::Context)

    FROST context.

  • secret_share (FROST::SecretShare)

    Signer secret key share.

  • group_pubkey (ECDSA::Point)

    Public key corresponding to the group signing key.

  • nonces (Array)

    Pair of nonce values (hiding_nonce, binding_nonce) for signer_i.

  • msg (String)

    The message to be signed

  • commitment_list (Array)

    A list of commitments issued by each participant.

Returns:

  • (Integer)

    A signature share.

Raises:

  • (ArgumentError)


123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/frost.rb', line 123

def sign(context, secret_share, group_pubkey, nonces, msg, commitment_list)
  raise ArgumentError "context must be FROST::Context." unless context.is_a?(FROST::Context)
  raise ArgumentError, "msg must be String." unless msg.is_a?(String)
  raise ArgumentError, "group_pubkey must be ECDSA::Point." unless group_pubkey.is_a?(ECDSA::Point)
  raise ArgumentError, "group_pubkey and context groups are different." unless context.group == group_pubkey.group

  identifier = secret_share.identifier
  # Compute binding factors
  binding_factors = compute_binding_factors(context, group_pubkey, commitment_list, msg)
  binding_factor = binding_factors[identifier]

  # Compute group commitment
  group_commitment = compute_group_commitment(commitment_list, binding_factors)

  # Compute Lagrange coefficient
  identifiers = commitment_list.map(&:identifier)
  lambda_i = Polynomial.derive_interpolating_value(identifiers, identifier, context.group)

  # Compute the per-message challenge
  challenge = compute_challenge(context, group_commitment, group_pubkey, msg)

  # Compute the signature share
  hiding_nonce, binding_nonce = context.convert_signer_nocnes(group_commitment, nonces)
  field = ECDSA::PrimeField.new(group_pubkey.group.order)
  field.mod(hiding_nonce.value +
              field.mod(binding_nonce.value * binding_factor) + field.mod(lambda_i * secret_share.share * challenge))
end

.verify(signature, public_key, msg) ⇒ Boolean

Verify signature.

Parameters:

Returns:

  • (Boolean)

    Verification result.

Raises:

  • (ArgumentError)


218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
# File 'lib/frost.rb', line 218

def verify(signature, public_key, msg)
  raise ArgumentError, "signature must be FROST::Signature" unless signature.is_a?(FROST::Signature)
  raise ArgumentError, "public_key must be ECDSA::Point" unless public_key.is_a?(ECDSA::Point)
  raise ArgumentError, "public_key and context groups are different." unless signature.context.group == public_key.group
  raise ArgumentError, "msg must be String." unless msg.is_a?(String)

  context = signature.context
  public_key, signature = context.pre_verify(public_key, signature)

  # Compute challenge
  challenge = compute_challenge(context, signature.r, public_key, msg)

  s_g = public_key.group.generator * signature.s
  c_p = public_key * challenge
  result = (s_g + signature.r.negate + c_p.negate) * public_key.group.cofactor
  result.infinity?
end

.verify_share(context, identifier, pubkey_i, sig_share_i, commitment_list, group_pubkey, msg) ⇒ Boolean

Verify signature share. in round two from the i-th participant.

Parameters:

  • context (FROST::Context)

    FROST context.

  • identifier (Integer)

    Identifier i of the participant.

  • pubkey_i (ECDSA::Point)

    The public key for the i-th participant

  • sig_share_i (Integer)

    Integer value indicating the signature share as produced

  • commitment_list (Array)

    A list of commitments issued by each participant.

  • group_pubkey (ECDSA::Point)

    Public key corresponding to the group signing key.

  • msg (String)

    The message to be signed.

Returns:

  • (Boolean)

    Verification result.

Raises:

  • (ArgumentError)


187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/frost.rb', line 187

def verify_share(context, identifier, pubkey_i, sig_share_i, commitment_list, group_pubkey, msg)
  raise ArgumentError "context must be FROST::Context." unless context.is_a?(FROST::Context)
  raise ArgumentError, "identifier must be Integer." unless identifier.is_a?(Integer)
  raise ArgumentError, "sig_share_i must be Integer." unless sig_share_i.is_a?(Integer)
  raise ArgumentError, "pubkey_i must be ECDSA::Point." unless pubkey_i.is_a?(ECDSA::Point)
  raise ArgumentError, "group_pubkey must be ECDSA::Point." unless group_pubkey.is_a?(ECDSA::Point)
  raise ArgumentError, "group_pubkey and context groups are different." unless context.group == group_pubkey.group

  binding_factors = compute_binding_factors(context, group_pubkey, commitment_list, msg)
  binding_factor = binding_factors[identifier]
  group_commitment = compute_group_commitment(commitment_list, binding_factors)
  comm_i = commitment_list.find{|c| c.identifier == identifier}
  hiding_commitment = comm_i.hiding
  binding_commitment = comm_i.binding
  raise ArgumentError, "hiding_commitment must be ECDSA::Point." unless hiding_commitment.is_a?(ECDSA::Point)
  raise ArgumentError, "binding_commitment must be ECDSA::Point." unless binding_commitment.is_a?(ECDSA::Point)

  comm_share = context.convert_commitment_share(group_commitment, hiding_commitment + binding_commitment * binding_factor)
  challenge = compute_challenge(context, group_commitment, group_pubkey, msg)
  identifiers = commitment_list.map(&:identifier)
  lambda_i = Polynomial.derive_interpolating_value(identifiers, identifier, context.group)
  l = context.group.generator * sig_share_i
  r = comm_share + pubkey_i * (challenge * lambda_i)
  l == r
end