bls12-381 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2660bb6049da204d945d3d40a6bf06f26ed1c041794d959ea9b0662b725a1b8b
4
- data.tar.gz: ad30c4cc593a729823bd51d05a1bdecc68566ed0e5b100cd31dc8130bf6e1353
3
+ metadata.gz: 1398ff4737e7cc35aee61178bb064235e4f5765ec18a69511681c9389c1384a5
4
+ data.tar.gz: f6f386f4681e0cb3a3c642a0d5fa2e061fc9c96b4034c019cdf0e510020e059a
5
5
  SHA512:
6
- metadata.gz: eab97d8693ad91b412a1f7c8dd362b0b82c241e96c220ba686e79208c0828b3cd27303ecc8fa68ae51e18ca1f596d87bceff9b35ab009795e438784b8cab66a6
7
- data.tar.gz: f1672d9beb73fa230f0d1bfe1b2ef56955599ed79d46ebd9a98cf344f593f705e6a63351dd87c7e65b8de25867705e915ae3f04d8e047ab9bd01741219dd5345
6
+ metadata.gz: 7589e44db5a83ae84615cfbad287d7c36c3eb223186e3b0552755ef36ceff516b8fcf40a1f11fec534344d48db49f4277be8fdbb82eb16e5f972854334732818
7
+ data.tar.gz: b0d5fb49ba941dec4fe7e3eb892cc08036e10505bf6cc6e01f429cf3874219092aaffb530bd54e96ba48c7ce01aa59cd4774871c81d05b72282e76f3a78757be
data/README.md CHANGED
@@ -4,7 +4,7 @@ This library is a Ruby BLS12-381 implementation based on the JavaScript implemen
4
4
  In addition to that, it is possible to switch between public key and signature group (G1 and G2).
5
5
 
6
6
  Note: This library has passed the same tests as noble-bls12-381, but has not been audited to prove its safety.
7
- Please be careful when using this.
7
+ Please be careful when using this, and read [Side channels](#side-channels) before signing with it.
8
8
 
9
9
  ## Installation
10
10
 
@@ -49,18 +49,12 @@ signature.to_signature
49
49
  is_correct = BLS.verify(signature, message, public_key)
50
50
  => true
51
51
 
52
- # Sign 1 msg with 3 keys
53
52
  private_keys = [
54
53
  '18f020b98eb798752a50ed0563b079c125b0db5dd0b1060d1c1b47d4a193e1e4',
55
54
  'ed69a8c50cf8c9836be3b67c7eeff416612d45ba39a5c099d48fa668bf558c9c',
56
55
  '16ae669f3be7a2121e17d0c68c05a8f3d6bef21ec0f2315f1d7aec12484e4cf5'
57
56
  ]
58
57
  public_keys = private_keys.map { |p| BLS.get_public_key(p) }
59
- signatures2 = private_keys.map { |p| BLS.sign(message, p) }
60
- agg_public_keys2 = BLS.aggregate_public_keys(public_keys)
61
- agg_signatures2 = BLS.aggregate_signatures(signatures2)
62
- is_correct2 = BLS.verify(agg_signatures2, message, agg_public_keys2)
63
- => true
64
58
 
65
59
  # Sign 3 msgs with 3 keys
66
60
  messages = %w[d2 0d98 05caf3]
@@ -68,8 +62,102 @@ signatures3 = private_keys.map.with_index { |p, i| BLS.sign(messages[i], p)}
68
62
  agg_signatures3 = BLS.aggregate_signatures(signatures3)
69
63
  is_correct3 = BLS.verify_batch(agg_signatures3, messages, public_keys)
70
64
  => true
65
+
66
+ # Sign 1 msg with 3 keys.
67
+ # This needs the proof of possession scheme: see the warning below.
68
+ proofs = private_keys.map { |p| BLS.pop_prove(p) }
69
+ public_keys.zip(proofs).each { |pubkey, proof| raise 'invalid key' unless BLS.pop_verify(pubkey, proof) }
70
+
71
+ signatures2 = private_keys.map { |p| BLS.sign(message, p, scheme: :pop) }
72
+ agg_signatures2 = BLS.aggregate_signatures(signatures2)
73
+ is_correct2 = BLS.fast_aggregate_verify(agg_signatures2, message, public_keys)
74
+ => true
71
75
  ```
72
76
 
77
+ ## Signature schemes
78
+
79
+ This library implements two of the schemes of
80
+ [draft-irtf-cfrg-bls-signature](https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-bls-signature-05),
81
+ selected with the `scheme:` keyword of `BLS.sign` / `BLS.verify` / `BLS.verify_batch`.
82
+ They use different domain separation tags, so a signature made under one never verifies
83
+ under the other. The default is `:basic`, which is what earlier versions of this gem used.
84
+
85
+ | scheme | aggregation |
86
+ | --- | --- |
87
+ | `:basic` (default) | `BLS.verify_batch`, **only when every message is distinct** |
88
+ | `:pop` | the above with no such restriction, plus `BLS.fast_aggregate_verify` for many signatures over a single message |
89
+
90
+ `BLS.verify_batch` returns `false` for a `:basic` aggregate that covers the same message
91
+ twice, however well formed the signature is. Pass `scheme: :pop` to verify those.
92
+
93
+ **Aggregating public keys is unsafe without proofs of possession.** Given a public key
94
+ `pk_victim`, an attacker can register `pk_attacker = g * x - pk_victim` for an `x` they
95
+ choose. The two keys aggregate to `g * x`, so the attacker alone can produce a signature
96
+ that verifies against the aggregate, making it look like the victim signed. This is the
97
+ rogue key attack, and it is why `:basic` supports no single-message aggregation at all.
98
+
99
+ `BLS.pop_prove` closes the hole by signing a key with itself: producing a proof requires
100
+ the private key, which the attacker does not have for `pk_attacker`. Call `BLS.pop_verify`
101
+ on every public key **before** passing it to `BLS.aggregate_public_keys` or
102
+ `BLS.fast_aggregate_verify` — neither of them can do it for you, since they never see the
103
+ proofs.
104
+
105
+ ## Errors
106
+
107
+ Everything this library raises about the data it was handed descends from `BLS::Error`, so
108
+ one rescue covers a key that will not parse, a point outside the subgroup, a private key that
109
+ is not hex, and a pairing that is not defined:
110
+
111
+ ```ruby
112
+ begin
113
+ public_key = BLS::PointG1.from_hex(untrusted)
114
+ rescue BLS::Error => e # BLS::PointError here, and BLS::PairingError elsewhere
115
+ ...
116
+ end
117
+ ```
118
+
119
+ Passing the wrong type or the wrong number of coefficients is a mistake in the calling code
120
+ rather than a fact about a key, and raises `ArgumentError` as it would anywhere else in Ruby.
121
+
122
+ Verification never raises over the data it is given. `BLS.verify`, `BLS.verify_batch`,
123
+ `BLS.fast_aggregate_verify` and `BLS.pop_verify` return `false` for anything they will not
124
+ accept, including the point at infinity and an empty set of signers, so untrusted input cannot
125
+ turn into an exception. Deserializing that input with `from_hex` beforehand still raises, which
126
+ is where a malformed encoding is caught.
127
+
128
+ ## Threads
129
+
130
+ Signing and verifying from several threads gives the right answers, but two caches are
131
+ written without a lock and neither is worth sharing a point over.
132
+
133
+ `calc_multiply_precomputes` builds its table over long enough that MRI switches threads part
134
+ way through, so callers racing on one point all pass the "already has precomputes" check and
135
+ each build their own. They build the same table, so the result stays correct; what you lose is
136
+ the time and the memory of doing it several times over, and a guard that reads as protection
137
+ while providing none. Build the table once, before the point goes anywhere else.
138
+
139
+ `BLS.verify` memoises pairing coefficients onto the G2 point it is given, and onto
140
+ `BLS::PointG2::BASE` when the public key is a G2 point. Racing callers recompute rather than
141
+ corrupt, for the same reason: what is assigned is always a finished array.
142
+
143
+ ## Side channels
144
+
145
+ **Nothing here is constant time, and signing is not safe against an attacker who can
146
+ measure it.** `BLS.sign` and `BLS.get_public_key` multiply a curve point by the private
147
+ key, and that multiplication leaks:
148
+
149
+ * the scalar picks which precomputed point each window adds, so it decides which memory
150
+ gets touched, which is visible through the cache;
151
+ * point addition returns early when an operand is the identity or the two are equal;
152
+ * underneath it all, Ruby's bignum arithmetic and the `%` in `BLS::Fp` take time that
153
+ depends on their operands.
154
+
155
+ A pure Ruby implementation cannot fix this: it has no control over how the interpreter
156
+ lays out or times its integers. Treat this gem as suitable where the private key operation
157
+ cannot be observed — verification of untrusted input is fine, since it touches no secret —
158
+ and use a binding to a constant time library such as [blst](https://github.com/supranational/blst)
159
+ where an attacker can watch the signer.
160
+
73
161
  ## License
74
162
 
75
163
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/lib/bls/error.rb ADDED
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BLS
4
+
5
+ # Base class for everything this library raises about the data it was given, so that
6
+ # `rescue BLS::Error` catches all of it.
7
+ #
8
+ # A wrong type or arity is a mistake in the calling code rather than a fact about a key or a
9
+ # point, and still raises ArgumentError, as it would anywhere else in Ruby.
10
+ class Error < StandardError; end
11
+
12
+ # A point that could not be parsed, is not on the curve, or is not in the prime-order
13
+ # subgroup.
14
+ class PointError < Error; end
15
+
16
+ # A pairing that was asked for where it is not defined.
17
+ class PairingError < Error; end
18
+ end
data/lib/bls/field.rb CHANGED
@@ -13,7 +13,15 @@ module BLS
13
13
  self.class.new(-value)
14
14
  end
15
15
 
16
+ # Multiplicative inverse.
17
+ # Zero has none, and the extended Euclid below returns 0 for it rather than failing, which
18
+ # is a wrong answer that propagates: ProjectivePoint#to_affine inverts z, so the point at
19
+ # infinity would come back as the affine coordinates (0, 0) instead of being refused.
20
+ # @return [BLS::Field] the inverse.
21
+ # @raise [BLS::Error] Occur when this element is zero.
16
22
  def invert
23
+ raise BLS::Error, 'Zero has no multiplicative inverse.' if zero?
24
+
17
25
  x0 = 1
18
26
  x1 = 0
19
27
  y0 = 0
@@ -248,7 +256,15 @@ module BLS
248
256
  Fp2.new([a * b, c * c1])
249
257
  end
250
258
 
259
+ # Square root, or nil when this element is not a square.
260
+ # Zero is answered up front. It is a square, its root being itself, and the division below
261
+ # would otherwise be asked for the inverse of zero. That used to yield zero and land here
262
+ # as a nil, which reads as "not a square" and is wrong for the one element that has
263
+ # exactly one root.
264
+ # @return [BLS::Fp2, nil]
251
265
  def sqrt
266
+ return self if zero?
267
+
252
268
  candidate = pow((Fp2::ORDER + 8) / 16)
253
269
  check = candidate.square / self
254
270
  r = ROOTS_OF_UNITY
@@ -700,12 +716,17 @@ module BLS
700
716
 
701
717
 
702
718
 
719
+ # sgn0 for GF(p^2), from RFC 9380 section 4.1.
720
+ # Combined with | and & rather than || and &&: sign_0 is 0 or 1, and 0 is truthy in Ruby,
721
+ # so a disjunction would always short circuit on it and x_1 would never be reached.
722
+ # @param [BLS::Fp2] x
723
+ # @return [Integer] 0 or 1.
703
724
  def sgn0(x)
704
725
  x0, x1 = x.values
705
726
  sign_0 = x0 % 2
706
- zero_0 = x0 === 0
727
+ zero_0 = x0.zero? ? 1 : 0
707
728
  sign_1 = x1 % 2
708
- sign_0 || (zero_0 && sign_1)
729
+ sign_0 | (zero_0 & sign_1)
709
730
  end
710
731
 
711
732
  def sqrt_div_fp2(u, v)
data/lib/bls/h2c.rb CHANGED
@@ -7,15 +7,17 @@ module BLS
7
7
 
8
8
  # @param [String] message hash value with hex format.
9
9
  # @param [Integer] len_in_bytes length
10
+ # @param [String] dst Domain separation tag. Mixing tags across schemes breaks the
11
+ # domain separation the ciphersuites rely on, so it must always be passed explicitly.
10
12
  # @return [Array[Integer]] byte array.
11
13
  # @raise BLS::Error
12
- def expand_message_xmd(message, len_in_bytes)
14
+ def expand_message_xmd(message, len_in_bytes, dst)
13
15
  b_in_bytes = BigDecimal(SHA256_DIGEST_SIZE)
14
16
  r_in_bytes = b_in_bytes * 2
15
17
  ell = (BigDecimal(len_in_bytes) / b_in_bytes).ceil
16
18
  raise BLS::Error, 'Invalid xmd length' if ell > 255
17
19
 
18
- dst_prime = PointG2::DST_BASIC.bytes + BLS.i2osp(PointG2::DST_BASIC.bytesize, 1)
20
+ dst_prime = dst.bytes + BLS.i2osp(dst.bytesize, 1)
19
21
  z_pad = BLS.i2osp(0, r_in_bytes)
20
22
  l_i_b_str = BLS.i2osp(len_in_bytes, 2)
21
23
  b = Array.new(ell)
@@ -34,12 +36,13 @@ module BLS
34
36
 
35
37
  # Convert hash to Field.
36
38
  # @param [String] message hash value with hex format.
39
+ # @param [String] dst Domain separation tag of the ciphersuite in use.
37
40
  # @return [Array[Integer]] byte array.
38
- def hash_to_field(message, random_oracle: true)
41
+ def hash_to_field(message, dst = PointG2::DST_BASIC, random_oracle: true)
39
42
  degree = 2
40
43
  count = random_oracle ? 2 : 1
41
44
  len_in_bytes = count * degree * LENGTH
42
- pseudo_random_bytes = BLS::H2C.expand_message_xmd(message, len_in_bytes)
45
+ pseudo_random_bytes = BLS::H2C.expand_message_xmd(message, len_in_bytes, dst)
43
46
  u = Array.new(count)
44
47
  count.times do |i|
45
48
  e = Array.new(degree)
data/lib/bls/math.rb CHANGED
@@ -66,13 +66,33 @@ module BLS
66
66
  end
67
67
 
68
68
  # Normalize private key.
69
+ # A key at or above the group order is reduced rather than rejected, matching the reference
70
+ # implementation and the test vectors. Reducing raw entropy that way is slightly biased, so
71
+ # prefer drawing the key from 0 < k < Fr::ORDER to begin with.
69
72
  # @param [String|Integer] private_key a private key with hex or number.
70
73
  # @return [BLS::Fr] Normalized private key.
71
- # @raise [BLS::Error] Occur when the private key is zero.
74
+ # @raise [BLS::Error] Occur when the private key is not a positive hex string or integer,
75
+ # or when it reduces to zero.
72
76
  def normalize_priv_key(private_key)
73
- k = private_key.is_a?(String) ? private_key.to_i(16) : private_key
74
- raise BLS::Error, "Private key must be 0 < private_key < #{Fr::ORDER}" if k <= 0
75
- Fr.new(k)
77
+ k = case private_key
78
+ when String
79
+ # String#to_i stops at the first character it cannot read and returns what it has,
80
+ # so a mistyped or truncated key would otherwise turn into a different, much
81
+ # smaller one without any sign that it happened.
82
+ raise BLS::Error, 'Private key must be a hex string.' unless private_key.match?(/\A[0-9a-fA-F]+\z/)
83
+
84
+ private_key.to_i(16)
85
+ when Integer
86
+ private_key
87
+ else
88
+ raise BLS::Error, 'Private key must be Integer or String.'
89
+ end
90
+ raise BLS::Error, 'Private key must be positive.' unless k.positive?
91
+
92
+ key = Fr.new(k)
93
+ raise BLS::Error, "Private key must not be a multiple of #{Fr::ORDER}." if key.zero?
94
+
95
+ key
76
96
  end
77
97
 
78
98
  # Convert number to +byte_length+ bytes hex string.
data/lib/bls/pairing.rb CHANGED
@@ -1,8 +1,6 @@
1
1
  module BLS
2
2
  module_function
3
3
 
4
- class PairingError < StandardError; end
5
-
6
4
  # @param [BLS::PointG1] p
7
5
  # @param [BLS::PointG2] q
8
6
  # @param [Boolean] with_final_exp
@@ -20,4 +18,19 @@ module BLS
20
18
  with_final_exp ? looped.final_exponentiate : looped
21
19
  end
22
20
 
21
+ # Pairing without the final exponentiation, using e(O, Q) = e(P, O) = 1 where {pairing}
22
+ # raises instead. Verification multiplies several of these together, so a degenerate key
23
+ # or signature has to fall out as a failed check rather than as an exception.
24
+ # @param [BLS::PointG1] p
25
+ # @param [BLS::PointG2] q
26
+ # @return [BLS::Fp12]
27
+ # @raise [ArgumentError]
28
+ def partial_pairing(p, q)
29
+ raise ArgumentError, 'p should be BLS::PointG1 object' unless p.is_a?(BLS::PointG1)
30
+ raise ArgumentError, 'q should be BLS::PointG2 object' unless q.is_a?(BLS::PointG2)
31
+ return Fp12::ONE if p.zero? || q.zero?
32
+
33
+ pairing(p, q, with_final_exp: false)
34
+ end
35
+
23
36
  end
data/lib/bls/point/g1.rb CHANGED
@@ -1,7 +1,10 @@
1
1
  module BLS
2
2
  class PointG1 < BLS::ProjectivePoint
3
3
 
4
+ # Ciphersuite IDs of draft-irtf-cfrg-bls-signature section 4.3.
4
5
  DST_BASIC = 'BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_'
6
+ DST_POP = 'BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_POP_'
7
+ DST_POP_PROOF = 'BLS_POP_BLS12381G1_XMD:SHA-256_SSWU_RO_POP_'
5
8
 
6
9
  KEY_SIZE_COMPRESSED = 48
7
10
  KEY_SIZE_UNCOMPRESSED = 96
@@ -13,33 +16,55 @@ module BLS
13
16
  # Parse PointG1 from form hex.
14
17
  # @param [String] hex hex value of PointG1.
15
18
  # @return [PointG1]
16
- # @raise [BLS::PointError] Occurs when hex length does not match, or point does not on G1.
19
+ # @raise [BLS::PointError] Occurs when hex length does not match, or point is not on G1.
17
20
  def self.from_hex(hex)
21
+ validate_hex!(hex)
18
22
  bytes = [hex].pack('H*')
19
- point = case bytes.bytesize
20
- when KEY_SIZE_COMPRESSED
21
- compressed_value = hex.to_i(16)
22
- b_flag = BLS.mod(compressed_value, POW_2_383) / POW_2_382
23
- return ZERO if b_flag == 1
24
-
25
- x = BLS.mod(compressed_value, POW_2_381)
26
- full_y = BLS.mod(x**3 + Fp.new(Curve::B).value, Curve::P)
23
+ unless [KEY_SIZE_COMPRESSED, KEY_SIZE_UNCOMPRESSED].include?(bytes.bytesize)
24
+ raise PointError, 'Invalid point G1, expected 48 or 96 bytes.'
25
+ end
26
+
27
+ m_byte = bytes[0].unpack1('C') & 0xe0
28
+ if [0x20, 0x60, 0xe0].include?(m_byte)
29
+ raise PointError, "Invalid encoding flag: #{m_byte.to_s(16)}"
30
+ end
31
+
32
+ c_bit = m_byte & POINT_COMPRESSION_FLAG # compression flag
33
+ i_bit = m_byte & POINT_INFINITY_FLAG # infinity flag
34
+ s_bit = m_byte & POINT_Y_FLAG # y coordinate sign flag
35
+ bytes[0] = [bytes[0].unpack1('C') & 0x1f].pack('C') # set flag to 0
36
+
37
+ if i_bit == POINT_INFINITY_FLAG && bytes.unpack1('H*').to_i(16) > 0
38
+ raise PointError, 'Invalid point, infinity point should be all 0.'
39
+ end
40
+
41
+ point = if bytes.bytesize == KEY_SIZE_COMPRESSED && c_bit == POINT_COMPRESSION_FLAG # compressed format
42
+ return ZERO if i_bit == POINT_INFINITY_FLAG
43
+
44
+ x = bytes.unpack1('H*').to_i(16)
45
+ raise PointError, 'Invalid point G1, x must be less than the field order.' unless x < Curve::P
46
+
47
+ full_y = BLS.mod(x**3 + Curve::B, Curve::P)
27
48
  y = BLS.pow_mod(full_y, (Curve::P + 1) / 4, Curve::P)
28
- raise PointError, 'The given point is not on G1: y**2 = x**3 + b.' unless (BLS.pow_mod(y, 2, Curve::P) - full_y).zero?
49
+ raise PointError, 'The given point is not on G1: y**2 = x**3 + b.' unless BLS.pow_mod(y, 2, Curve::P) == full_y
29
50
 
30
- a_flag = BLS.mod(compressed_value, POW_2_382) / POW_2_381
31
- y = Curve::P - y unless ((y * 2) / Curve::P) == a_flag
51
+ y = Curve::P - y unless ((y * 2) / Curve::P) == (s_bit.zero? ? 0 : 1)
32
52
  PointG1.new(Fp.new(x), Fp.new(y), Fp::ONE)
33
- when KEY_SIZE_UNCOMPRESSED
34
- return ZERO unless (bytes[0].unpack1('H*').to_i(16) & (1 << 6)).zero?
53
+ elsif bytes.bytesize == KEY_SIZE_UNCOMPRESSED && c_bit != POINT_COMPRESSION_FLAG # uncompressed format
54
+ return ZERO if i_bit == POINT_INFINITY_FLAG
35
55
 
36
56
  x = bytes[0...PUBLIC_KEY_LENGTH].unpack1('H*').to_i(16)
37
57
  y = bytes[PUBLIC_KEY_LENGTH..-1].unpack1('H*').to_i(16)
58
+ unless x < Curve::P && y < Curve::P
59
+ raise PointError, 'Invalid point G1, coordinates must be less than the field order.'
60
+ end
61
+
38
62
  PointG1.new(Fp.new(x), Fp.new(y), Fp::ONE)
39
63
  else
40
- raise PointError, 'Invalid point G1, expected 48 or 96 bytes.'
64
+ raise PointError, 'Invalid point G1, compression flag does not match the encoded length.'
41
65
  end
42
66
  point.validate!
67
+ point.validate_group!
43
68
  point
44
69
  end
45
70
 
@@ -73,12 +98,15 @@ module BLS
73
98
 
74
99
  # Convert hash to PointG1
75
100
  # @param [String] message a hash with hex format.
101
+ # @param [Symbol] scheme signature scheme whose domain separation tag is used, :basic or :pop.
102
+ # @param [String] dst a domain separation tag to use instead of the scheme's, for a
103
+ # ciphersuite this library does not name, such as the ones the RFC 9380 vectors use.
76
104
  # @return [BLS::PointG1] point.
77
105
  # @raise [BLS::PointError]
78
- def self.hash_to_curve(message)
79
- raise PointError, 'expected hex string' unless message[/^[a-fA-F0-9]*$/]
106
+ def self.hash_to_curve(message, scheme: :basic, dst: nil)
107
+ validate_hex!(message)
80
108
 
81
- h2c = ::H2C.get(::H2C::Suite::BLS12381G1_XMDSHA256_SSWU_RO_, PointG1::DST_BASIC)
109
+ h2c = ::H2C.get(::H2C::Suite::BLS12381G1_XMDSHA256_SSWU_RO_, dst || self.dst(scheme))
82
110
  p = h2c.digest([message].pack('H*'))
83
111
 
84
112
  PointG1.new(Fp.new(p.x), Fp.new(p.y), Fp::ONE)
data/lib/bls/point/g2.rb CHANGED
@@ -3,7 +3,10 @@ module BLS
3
3
 
4
4
  attr_accessor :precomputes
5
5
 
6
+ # Ciphersuite IDs of draft-irtf-cfrg-bls-signature section 4.2.
6
7
  DST_BASIC = 'BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_'
8
+ DST_POP = 'BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_'
9
+ DST_POP_PROOF = 'BLS_POP_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_'
7
10
 
8
11
  KEY_SIZE_COMPRESSED = 96
9
12
  KEY_SIZE_UNCOMPRESSED = 192
@@ -18,7 +21,12 @@ module BLS
18
21
  # @return [BLS::PointG2] PointG2.
19
22
  # @raise [BLS::PointError]
20
23
  def self.from_hex(hex)
24
+ validate_hex!(hex)
21
25
  bytes = [hex].pack('H*')
26
+ unless [KEY_SIZE_COMPRESSED, KEY_SIZE_UNCOMPRESSED].include?(bytes.bytesize)
27
+ raise PointError, 'Invalid point G2, expected 96/192 bytes.'
28
+ end
29
+
22
30
  m_byte = bytes[0].unpack1('C')& 0xe0
23
31
  if [0x20, 0x60, 0xe0].include?(m_byte)
24
32
  raise PointError, "Invalid encoding flag: #{m_byte.to_s(16)}"
@@ -46,7 +54,11 @@ module BLS
46
54
  else
47
55
  (y.coeffs[1].value * 2) / Curve::P == 1 ? 1 : 0
48
56
  end
49
- y = s_bit > 0 && bit_y > 0 ? y : y.negate
57
+ # Flip to whichever root the encoding asked for. Written as a comparison, the
58
+ # way G1 does it, rather than as `s_bit > 0 && bit_y > 0 ? y : y.negate`:
59
+ # that form is only right because sqrt returns the root of sign 1, which it
60
+ # does not promise, and it would negate a root of sign 0 that wanted sign 0.
61
+ y = y.negate unless bit_y == (s_bit.zero? ? 0 : 1)
50
62
  PointG2.new(x, y, Fp2::ONE)
51
63
  elsif bytes.bytesize == KEY_SIZE_UNCOMPRESSED && c_bit != POINT_COMPRESSION_FLAG # uncompressed format
52
64
  return ZERO if i_bit == POINT_INFINITY_FLAG
@@ -56,9 +68,10 @@ module BLS
56
68
  y0 = bytes[(3 * PUBLIC_KEY_LENGTH)..-1].unpack1('H*').to_i(16)
57
69
  PointG2.new(Fp2.new([x0, x1]), Fp2.new([y0, y1]), Fp2::ONE)
58
70
  else
59
- raise PointError, 'Invalid point G2, expected 96/192 bytes.'
71
+ raise PointError, 'Invalid point G2, compression flag does not match the encoded length.'
60
72
  end
61
73
  point.validate!
74
+ point.validate_group!
62
75
  point
63
76
  end
64
77
 
@@ -72,12 +85,15 @@ module BLS
72
85
 
73
86
  # Convert hash to PointG2
74
87
  # @param [String] message a hash with hex format.
88
+ # @param [Symbol] scheme signature scheme whose domain separation tag is used, :basic or :pop.
89
+ # @param [String] dst a domain separation tag to use instead of the scheme's, for a
90
+ # ciphersuite this library does not name, such as the ones the RFC 9380 vectors use.
75
91
  # @return [BLS::PointG2] point.
76
92
  # @raise [BLS::PointError]
77
- def self.hash_to_curve(message)
78
- raise PointError, 'expected hex string' unless message[/^[a-fA-F0-9]*$/]
93
+ def self.hash_to_curve(message, scheme: :basic, dst: nil)
94
+ validate_hex!(message)
79
95
 
80
- u = BLS::H2C::G2.hash_to_field(message)
96
+ u = BLS::H2C::G2.hash_to_field(message, dst || self.dst(scheme))
81
97
  q0 = PointG2.new(*BLS::H2C::G2.isogeny_map(*BLS::H2C::G2.map_to_curve_sswu(u[0])))
82
98
  q1 = PointG2.new(*BLS::H2C::G2.isogeny_map(*BLS::H2C::G2.map_to_curve_sswu(u[1])))
83
99
  r = q0 + q1
@@ -137,6 +153,13 @@ module BLS
137
153
  self.precomputes = nil
138
154
  end
139
155
 
156
+ # The line coefficients {BLS.miller_loop} needs, worked out once and kept on the point.
157
+ #
158
+ # This memoises without a lock, so concurrent callers can each compute it. They compute
159
+ # the same coefficients and assign a finished array, so a reader sees one or the other and
160
+ # never a partial one; the cost is the repeated work. BLS.verify reaches this on
161
+ # PointG2::BASE, which is shared, whenever the public key is a G2 point.
162
+ # @return [Array]
140
163
  def pairing_precomputes
141
164
  return precomputes if precomputes
142
165
 
data/lib/bls/point.rb CHANGED
@@ -13,8 +13,6 @@ module BLS
13
13
  autoload :PointG1, "bls/point/g1"
14
14
  autoload :PointG2, "bls/point/g2"
15
15
 
16
- class PointError < StandardError; end
17
-
18
16
  # Abstract Point class that consist of projective coordinates.
19
17
  class ProjectivePoint
20
18
 
@@ -28,6 +26,40 @@ module BLS
28
26
  @m_precomputes = nil
29
27
  end
30
28
 
29
+ # Domain separation tag this point class uses for +scheme+.
30
+ # @param [Symbol] scheme :basic, :pop, or :pop_proof(the tag proofs of possession
31
+ # are signed under, kept separate from :pop so a proof cannot pass as a signature).
32
+ # @return [String] domain separation tag.
33
+ # @raise [BLS::Error] Occur when the scheme is unknown.
34
+ def self.dst(scheme)
35
+ case scheme
36
+ when :basic then const_get(:DST_BASIC)
37
+ when :pop then const_get(:DST_POP)
38
+ when :pop_proof then const_get(:DST_POP_PROOF)
39
+ else raise BLS::Error, "Unknown scheme: #{scheme.inspect}. Must be :basic or :pop."
40
+ end
41
+ end
42
+
43
+ # Check that +hex+ is a whole number of bytes written in hex, for {from_hex} and
44
+ # {hash_to_curve}, which both unpack their argument with pack('H*').
45
+ #
46
+ # pack maps a character to a nibble by its low bits, so it reads '3', '#', 'J', 'Z', 'j'
47
+ # and 'z' all as 3 and never complains, and it pads an odd number of digits out to a whole
48
+ # byte. Left alone, that gives every point and every message a large family of spellings
49
+ # that all arrive at the same bytes. Applications tend to carry keys around as hex and
50
+ # compare, deduplicate and index them that way, so the aliases matter as much here as the
51
+ # non-canonical encodings do a layer down.
52
+ #
53
+ # \A..\z rather than ^..$ because those match at line boundaries in Ruby, which would let
54
+ # a newline carry a non-hex tail past the check.
55
+ # @param [String] hex a byte string in hex format.
56
+ # @raise [PointError] Occur when it is not an even length string of hex digits.
57
+ def self.validate_hex!(hex)
58
+ return if hex.is_a?(String) && hex.match?(/\A(?:[0-9a-fA-F]{2})*\z/)
59
+
60
+ raise PointError, 'expected hex string'
61
+ end
62
+
31
63
  def zero?
32
64
  z.zero?
33
65
  end
@@ -109,6 +141,10 @@ module BLS
109
141
  end
110
142
  alias - subtract
111
143
 
144
+ # Scalar multiplication by plain double-and-add, which skips the addition entirely on a
145
+ # zero bit and so takes a number of operations that tracks the scalar's Hamming weight.
146
+ # Only for scalars an observer already knows, such as the curve parameters. {multiply}
147
+ # hides that particular pattern, but is not constant time either.
112
148
  def multiply_unsafe(scalar)
113
149
  n = scalar.is_a?(Field) ? scalar.value : scalar
114
150
  raise PointError, 'Point#multiply: invalid scalar, expected positive integer' if n <= 0
@@ -123,11 +159,30 @@ module BLS
123
159
  p
124
160
  end
125
161
 
162
+ # Check whether this point belongs to the prime-order subgroup (G1 or G2).
163
+ # Being on the curve is not sufficient: E(Fp) and E'(Fp2) both contain points
164
+ # outside the order-r subgroup, and accepting them makes keys and signatures malleable.
165
+ # Since r**2 does not divide the group order, [r]P == O holds only for P in the subgroup.
166
+ # @return [Boolean] true if this point is in the prime-order subgroup.
167
+ def in_group?
168
+ zero? || multiply_unsafe(Curve::R).zero?
169
+ end
170
+
171
+ # Validate that this point belongs to the prime-order subgroup.
172
+ # @raise [PointError] Occur when this point is not in the prime-order subgroup.
173
+ def validate_group!
174
+ raise PointError, 'Invalid point: not in prime-order subgroup' unless in_group?
175
+ end
176
+
126
177
  def to_affine(inv_z = z.invert)
127
178
  [x * inv_z, y * inv_z]
128
179
  end
129
180
 
181
+ # @raise [PointError] Occur when any of +points+ is the point at infinity, which has no
182
+ # affine representation.
130
183
  def to_affine_batch(points)
184
+ raise PointError, 'The point at infinity has no affine representation.' if points.any?(&:zero?)
185
+
131
186
  to_inv = gen_invert_batch(points.map(&:z))
132
187
  points.map.with_index { |p, i| p.to_affine(to_inv[i]) }
133
188
  end
@@ -136,21 +191,23 @@ module BLS
136
191
  new_point(xy[0], xy[1], x.class.const_get(:ONE))
137
192
  end
138
193
 
194
+ # Inverts a whole array for the price of one inversion plus a few multiplications each.
195
+ # Zero is refused rather than skipped: leaving it in place would hand the caller a zero
196
+ # where it asked for an inverse, and Field#invert refuses it for the same reason.
197
+ # @raise [BLS::Error] Occur when any element is zero.
139
198
  def gen_invert_batch(nums)
199
+ raise BLS::Error, 'Zero has no multiplicative inverse.' if nums.any?(&:zero?)
200
+
140
201
  len = nums.length
141
202
  scratch = Array.new(len)
142
203
  acc = x.class::ONE
143
204
  len.times do |i|
144
- next if nums[i].zero?
145
-
146
205
  scratch[i] = acc
147
206
  acc *= nums[i]
148
207
  end
149
208
  acc = acc.invert
150
209
  len.times do |t|
151
210
  i = len - t - 1
152
- next if nums[i].zero?
153
-
154
211
  tmp = acc * nums[i]
155
212
  nums[i] = acc * scratch[i]
156
213
  acc = tmp
@@ -158,7 +215,15 @@ module BLS
158
215
  nums
159
216
  end
160
217
 
161
- # Constant time multiplication. Uses wNAF.
218
+ # Scalar multiplication using wNAF.
219
+ #
220
+ # This is NOT constant time, despite what {multiply_unsafe} implies by contrast. wNAF
221
+ # gives every window an addition, but the point it adds is read at an index derived from
222
+ # the scalar, and the additions themselves branch on whether an operand is the identity
223
+ # or the two are equal. Underneath, Ruby's bignum arithmetic and the modulo in Fp both run
224
+ # in time that depends on their operands. Anything the scalar decides is therefore visible
225
+ # to something watching timing or cache behaviour, which matters here because #sign and
226
+ # .from_private_key reach this with the private key. See the README.
162
227
  def multiply(scalar)
163
228
  n = scalar.is_a?(Field) ? scalar.value : scalar
164
229
  raise PointError, 'Invalid scalar, expected positive integer' if n <= 0
@@ -193,6 +258,18 @@ module BLS
193
258
  to_affine_batch(points).map{ |p| from_affine_tuple(p) }
194
259
  end
195
260
 
261
+ # Build the window table that {multiply} then uses, trading memory for speed on a point
262
+ # that will be multiplied repeatedly, such as a generator.
263
+ #
264
+ # Call this once, before the point is shared between threads. The check below is not a
265
+ # lock: the table takes long enough to build that MRI will switch threads part way
266
+ # through, so several callers can pass the check together and each build their own, each
267
+ # paying the time and the memory. What they build is identical, so whichever assignment
268
+ # lands last is still correct and no half-built table is ever visible; the cost is the
269
+ # duplicated work, and a guard that reads as protection while providing none.
270
+ #
271
+ # @param [Integer] w window width.
272
+ # @raise [PointError] Occur when this point already has precomputes.
196
273
  def calc_multiply_precomputes(w)
197
274
  raise PointError, 'This point already has precomputes.' if m_precomputes
198
275
 
@@ -243,12 +320,12 @@ module BLS
243
320
  point.is_a?(PointG2) ? point : PointG2.from_hex(point)
244
321
  end
245
322
 
246
- def norm_p1h(point)
247
- point.is_a?(PointG1) ? point : PointG1.hash_to_curve(point)
323
+ def norm_p1h(point, scheme: :basic)
324
+ point.is_a?(PointG1) ? point : PointG1.hash_to_curve(point, scheme: scheme)
248
325
  end
249
326
 
250
- def norm_p2h(point)
251
- point.is_a?(PointG2) ? point : PointG2.hash_to_curve(point)
327
+ def norm_p2h(point, scheme: :basic)
328
+ point.is_a?(PointG2) ? point : PointG2.hash_to_curve(point, scheme: scheme)
252
329
  end
253
330
 
254
331
  end
data/lib/bls/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BLS
4
- VERSION = '0.3.1'
4
+ VERSION = '0.4.0'
5
5
  end
data/lib/bls.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'digest'
4
4
  require 'bls/version'
5
+ require 'bls/error' # defines the exception classes the files below raise
5
6
  require 'bls/math'
6
7
  require 'bls/curve'
7
8
  require 'bls/field'
@@ -11,8 +12,6 @@ require 'bls/pairing'
11
12
 
12
13
  module BLS
13
14
 
14
- class Error < StandardError; end
15
-
16
15
  POW_2_381 = 2**381
17
16
  POW_2_382 = POW_2_381 * 2
18
17
  POW_2_383 = POW_2_382 * 2
@@ -28,13 +27,15 @@ module BLS
28
27
  # @param [Symbol] sig_type Signature type, :g1 or :g2.
29
28
  # If :g1 is specified, the signature is a point on G1 and the public key is a point on G2.
30
29
  # If :g2 is specified, the signature is a point on G2 and the public key is a point on G1.
30
+ # @param [Symbol] scheme Signature scheme, :basic or :pop. Signatures made under one scheme
31
+ # do not verify under the other, so signer and verifier must agree on it.
31
32
  # @return [PointG2] The signature point.
32
- def sign(message, private_key, sig_type: :g2)
33
+ def sign(message, private_key, sig_type: :g2, scheme: :basic)
33
34
  msg_point = case sig_type
34
35
  when :g1
35
- BLS.norm_p1h(message)
36
+ BLS.norm_p1h(message, scheme: scheme)
36
37
  when :g2
37
- BLS.norm_p2h(message)
38
+ BLS.norm_p2h(message, scheme: scheme)
38
39
  else
39
40
  raise Error, 'sig_type must be :g1 or :g2.'
40
41
  end
@@ -62,30 +63,42 @@ module BLS
62
63
  # @param [BLS::PointG1|BLS::PointG2] signature
63
64
  # @param [String] message Message digest(hash value with hex format) to be verified.
64
65
  # @param [BLS::PointG2|BLS::PointG1] public_key Public key with hex format or PointG1.
66
+ # @param [Symbol] scheme Signature scheme the signature was made under, :basic or :pop.
65
67
  # @return [Boolean] verification result.
66
- def verify(signature, message, public_key)
68
+ def verify(signature, message, public_key, scheme: :basic)
67
69
  unless signature.is_a?(PointG1) && public_key.is_a?(PointG2) ||
68
70
  signature.is_a?(PointG2) && public_key.is_a?(PointG1)
69
71
  raise BLS::Error, 'Invalid signature or public key. If the public key is PointG1, the signature must be an element of Point::G2 or vice versa.'
70
72
  end
73
+ # KeyValidate of draft-irtf-cfrg-bls-signature section 2.5. The identity is not a public
74
+ # key anybody holds, and pairing it away would leave the identity signature verifying
75
+ # against every message.
76
+ return false if public_key.zero?
77
+
71
78
  g = public_key.is_a?(PointG1) ? PointG1::BASE : PointG2::BASE
72
79
  ephm = if public_key.is_a?(PointG1)
73
- hm = BLS.norm_p2h(message)
74
- BLS.pairing(public_key.negate, hm, with_final_exp: false)
80
+ hm = BLS.norm_p2h(message, scheme: scheme)
81
+ BLS.partial_pairing(public_key.negate, hm)
75
82
  else
76
- hm = BLS.norm_p1h(message)
77
- BLS.pairing(hm, public_key.negate, with_final_exp: false)
83
+ hm = BLS.norm_p1h(message, scheme: scheme)
84
+ BLS.partial_pairing(hm, public_key.negate)
78
85
  end
79
86
  egs = if public_key.is_a?(PointG1)
80
- BLS.pairing(g, signature, with_final_exp: false)
87
+ BLS.partial_pairing(g, signature)
81
88
  else
82
- BLS.pairing(signature, g, with_final_exp: false)
89
+ BLS.partial_pairing(signature, g)
83
90
  end
84
91
  exp = (egs * ephm).final_exponentiate
85
92
  exp == Fp12::ONE
86
93
  end
87
94
 
88
95
  # Aggregate multiple public keys.
96
+ #
97
+ # WARNING: the aggregate is only meaningful once every input key has been checked with
98
+ # {pop_verify}. Without that check an attacker who publishes pk_a = g * x - pk_victim
99
+ # can produce, on their own, a signature that verifies against the aggregate, making it
100
+ # look like the victim signed. See {fast_aggregate_verify}.
101
+ #
89
102
  # @param [Array[BLS::PointG1]|Array[BLS::PointG2]] public_keys the list of public keys.
90
103
  # @return [BLS::PointG1|BLS::PointG2] aggregated public key.
91
104
  def aggregate_public_keys(public_keys)
@@ -120,13 +133,20 @@ module BLS
120
133
  end
121
134
 
122
135
  # Verify aggregated signature.
136
+ #
137
+ # Under the :basic scheme the messages must all be distinct: repeating one is reported as
138
+ # an invalid signature, since that scheme has nothing but message distinctness to stop a
139
+ # rogue key from signing on another key's behalf. Use :pop, whose proofs of possession rule
140
+ # that out, to verify several signatures over the same message.
141
+ #
123
142
  # @param [BLS::PointG2|BLS::PointG1] signature aggregated signature(BLS::PointG2 or BLS::PointG1).
124
143
  # @param [Array[String]] messages the list of message.
125
144
  # @param [Array[BLS::PointG1]|Array[BLS::PointG2]] public_keys the list of public keys(BLS::PointG1 or BLS::PointG2).
145
+ # @param [Symbol] scheme Signature scheme the signatures were made under, :basic or :pop.
126
146
  # @return [Boolean] verification result.
127
- def verify_batch(signature, messages, public_keys)
128
- raise BLS::Error, 'Expected non-empty array.' if messages.empty?
147
+ def verify_batch(signature, messages, public_keys, scheme: :basic)
129
148
  raise BLS::Error, 'Public keys count should equal msg count.' unless messages.size == public_keys.size
149
+ return false if messages.empty? # nothing was signed, so nothing is verified
130
150
 
131
151
  sig_g2_flag = signature.is_a?(PointG2)
132
152
  public_keys.each do |public_key|
@@ -134,20 +154,70 @@ module BLS
134
154
  raise BLS::Error, "Public key must be #{sig_g2_flag ? 'PointG1' : 'PointG2'}"
135
155
  end
136
156
  end
157
+ return false if public_keys.any?(&:zero?) # KeyValidate, as in #verify
158
+
159
+ n_message = messages.map { |m| sig_g2_flag ? BLS.norm_p2h(m, scheme: scheme) : BLS.norm_p1h(m, scheme: scheme)}
137
160
 
138
- n_message = messages.map { |m| sig_g2_flag ? BLS.norm_p2h(m) : BLS.norm_p1h(m)}
139
- paired = []
161
+ # Keys that signed the same message are summed so that message is paired exactly once:
162
+ # e(P1, Q) * e(P2, Q) == e(P1 + P2, Q). Grouping on the serialized point rather than on
163
+ # the message keeps this independent of how equal messages happened to be spelled.
140
164
  zero = sig_g2_flag ? PointG1::ZERO : PointG2::ZERO
141
- n_message.each do |message|
142
- group_pubkey = n_message.each_with_index.inject(zero)do|group_pubkey, (sub_message, i)|
143
- sub_message == message ? group_pubkey + public_keys[i] : group_pubkey
144
- end
145
- paired << (sig_g2_flag ? BLS.pairing(group_pubkey, message, with_final_exp: false) :
146
- BLS.pairing(message, group_pubkey, with_final_exp: false))
165
+ grouped = {}
166
+ n_message.each_with_index do |message, i|
167
+ group = (grouped[message.to_hex(compressed: true)] ||= [message, zero])
168
+ group[1] += public_keys[i]
169
+ end
170
+ return false if scheme == :basic && grouped.size < n_message.size
171
+
172
+ paired = grouped.each_value.map do |message, group_pubkey|
173
+ sig_g2_flag ? BLS.partial_pairing(group_pubkey, message) :
174
+ BLS.partial_pairing(message, group_pubkey)
147
175
  end
148
- paired << (sig_g2_flag ? BLS.pairing(PointG1::BASE.negate, signature, with_final_exp: false) :
149
- BLS.pairing(signature, PointG2::BASE.negate, with_final_exp: false))
176
+ paired << (sig_g2_flag ? BLS.partial_pairing(PointG1::BASE.negate, signature) :
177
+ BLS.partial_pairing(signature, PointG2::BASE.negate))
150
178
  product = paired.inject(Fp12::ONE) { |a, b| a * b }
151
179
  product.final_exponentiate == Fp12::ONE
152
180
  end
181
+
182
+ # Generate a proof of possession for +private_key+: a signature, under that key, over the
183
+ # public key it belongs to. Producing one requires knowing the private key, which is what
184
+ # makes aggregation safe against rogue key attacks.
185
+ # @param [Integer|String] private_key The private key. Integer or String(hex).
186
+ # @param [Symbol] key_type Public key type, :g1 or :g2. The proof lives in the other group.
187
+ # @return [BLS::PointG2|BLS::PointG1] proof of possession.
188
+ def pop_prove(private_key, key_type: :g1)
189
+ public_key = get_public_key(private_key, key_type: key_type)
190
+ msg = public_key.to_hex(compressed: true)
191
+ msg_point = public_key.is_a?(PointG1) ? BLS.norm_p2h(msg, scheme: :pop_proof) :
192
+ BLS.norm_p1h(msg, scheme: :pop_proof)
193
+ msg_point * BLS.normalize_priv_key(private_key)
194
+ end
195
+
196
+ # Verify a proof of possession produced by {pop_prove}.
197
+ # @param [BLS::PointG1|BLS::PointG2] public_key the public key being proven.
198
+ # @param [BLS::PointG2|BLS::PointG1] proof proof of possession.
199
+ # @return [Boolean] verification result.
200
+ def pop_verify(public_key, proof)
201
+ verify(proof, public_key.to_hex(compressed: true), public_key, scheme: :pop_proof)
202
+ end
203
+
204
+ # Verify an aggregated signature over a single message signed by every key in +public_keys+.
205
+ #
206
+ # WARNING: every public key must have passed {pop_verify} first. This check cannot be done
207
+ # here because it needs each key's proof, and skipping it reopens the rogue key attack that
208
+ # the pop scheme exists to prevent.
209
+ #
210
+ # @param [BLS::PointG2|BLS::PointG1] signature aggregated signature.
211
+ # @param [String] message Message digest(hash value with hex format) to be verified.
212
+ # @param [Array[BLS::PointG1]|Array[BLS::PointG2]] public_keys the list of public keys.
213
+ # @return [Boolean] verification result.
214
+ def fast_aggregate_verify(signature, message, public_keys)
215
+ return false if public_keys.empty? # nothing signed, so nothing is verified
216
+ # KeyValidate every key, not just the aggregate. The identity adds nothing to the sum, so
217
+ # a set containing it would otherwise verify on the strength of the others while counting
218
+ # a key that nobody holds, and could hold, as one of the signers.
219
+ return false if public_keys.any?(&:zero?)
220
+
221
+ verify(signature, message, aggregate_public_keys(public_keys), scheme: :pop)
222
+ end
153
223
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bls12-381
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shigeyuki Azuchi
@@ -93,6 +93,7 @@ files:
93
93
  - bls12-381.gemspec
94
94
  - lib/bls.rb
95
95
  - lib/bls/curve.rb
96
+ - lib/bls/error.rb
96
97
  - lib/bls/field.rb
97
98
  - lib/bls/h2c.rb
98
99
  - lib/bls/math.rb