bls12-381 0.4.0 → 0.5.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: 1398ff4737e7cc35aee61178bb064235e4f5765ec18a69511681c9389c1384a5
4
- data.tar.gz: f6f386f4681e0cb3a3c642a0d5fa2e061fc9c96b4034c019cdf0e510020e059a
3
+ metadata.gz: 24158dde28a5367b3fd19c5d77dfeb70ba7429c43f501f1e15345dc5792cfcd2
4
+ data.tar.gz: ee9dd866975f45538e1bd4cfa044fd88741cd0e01176fc6dea075111eab88cfe
5
5
  SHA512:
6
- metadata.gz: 7589e44db5a83ae84615cfbad287d7c36c3eb223186e3b0552755ef36ceff516b8fcf40a1f11fec534344d48db49f4277be8fdbb82eb16e5f972854334732818
7
- data.tar.gz: b0d5fb49ba941dec4fe7e3eb892cc08036e10505bf6cc6e01f429cf3874219092aaffb530bd54e96ba48c7ce01aa59cd4774871c81d05b72282e76f3a78757be
6
+ metadata.gz: 1751863707f75ea0d6bcc0fe8d4e1e1da82fb9657256f4a272efb129288dfec205be049dd6dde478f96488dcbf083d9131ba50779f3467f364a2b6c8659e1f4a
7
+ data.tar.gz: f523b7e1d92e900b44aa395359a9fa2a98e701a381e5b217d934cd50fa9f194908f5789ee197047007e3b80f41cf2a67fcbbb224779219292b4c8ab557b4bfd7
data/CHANGELOG.md CHANGED
@@ -1,5 +1,62 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.5.0] - 2026-11-02
4
+
5
+ ### Fixed
6
+
7
+ - `PointG2.from_hex` now rejects a coordinate at or above the field order. A coordinate written as `c + P` used to decode to the same point as `c`, giving one point several encodings.
8
+ - `expand_message_xmd`, and so `hash_to_curve` with a custom `dst:`, handles a domain separation tag longer than 255 bytes by hashing it as RFC 9380 section 5.3.3 prescribes, instead of failing inside I2OSP.
9
+ - Raising a field element to a negative power raises `ArgumentError`; `Fp2`, `Fp6` and `Fp12` used to answer with one.
10
+ - `expand_message_xmd` computes exactly the blocks RFC 9380 asks for. It used to compute one more, which cost a SHA-256 per call and failed outright at the largest permitted length.
11
+ - `==` on `Fp2`, `Fp6` and `Fp12` returns false for anything that is not the same kind of element instead of raising `NoMethodError`.
12
+ - `==` on a point returns false for a point of the other group, nil or anything else that is not a point of the same group, instead of raising `BLS::PointError`.
13
+ - `BLS.pairing` refuses a point outside the prime-order subgroup with `BLS::PointError`; it used to check only that the point was on the curve.
14
+
15
+ ### Changed
16
+
17
+ - Scalar multiplication sizes its wNAF window table from the 255 bits of the group order rather than the base field, reduces the scalar modulo the order first, and skips zero windows instead of adding a throwaway point. `multiply` on G2 runs about three times faster and precompute tables shrink to a third.
18
+ - Subgroup membership (`in_group?`, and so `from_hex`) uses the endomorphism tests of Scott's "A note on group membership tests for G1, G2 and GT on BLS pairing-friendly curves" in place of a multiplication by the group order.
19
+ - `BLS::PointG1::MAX_BITS` and `BLS::PointG2::MAX_BITS` are gone; `BLS::ProjectivePoint::SCALAR_BITS` holds the width that matters.
20
+ - `BLS.norm_p1` and `BLS.norm_p2`, which nothing used, are gone.
21
+ - The `bigdecimal` dependency is gone.
22
+ - Requires Ruby 3.3 or later, matching what is tested.
23
+ - Depend on h2c 0.4.x, which no longer defines a top-level `BLS` module of its own.
24
+ - `hash_to_curve` refuses an empty or non-string `dst:` with `BLS::Error` for both groups, as RFC 9380 section 3.1 requires, and anything the h2c gem raises for G1 arrives as `BLS::PointError` rather than `H2C::Error`.
25
+
26
+ ## [0.4.0] - 2026-08-10
27
+
28
+ - Check that every parsed point lies in the prime-order subgroup.
29
+ - Add the proof of possession scheme: `scheme:` on `sign`, `verify` and `verify_batch`, plus `pop_prove`, `pop_verify` and `fast_aggregate_verify`.
30
+ - `verify_batch` handles repeated messages correctly and refuses them under `:basic`.
31
+ - Verification returns false for the point at infinity instead of raising.
32
+ - Validate G1 encodings as strictly as G2: infinity padding, flag and length consistency, reduced coordinates.
33
+ - Validate hex input for points, messages and private keys instead of silently truncating or aliasing it.
34
+ - `Field#invert` raises on zero instead of returning zero.
35
+ - Fix `sgn0` to consult the second coordinate when the first is zero.
36
+ - All exceptions descend from `BLS::Error`, with `BLS::PointError` and `BLS::PairingError` beneath it.
37
+ - Add the bls12-381-tests and RFC 9380 test vectors. Document that nothing here is constant time.
38
+
39
+ ## [0.3.1] - 2026-04-24
40
+
41
+ - Ruby 3.4 compatibility: depend on `bigdecimal` explicitly, follow the h2c gem's SSWU rename.
42
+
43
+ ## [0.3.0] - 2023-07-21
44
+
45
+ - Support the compressed PointG2 format.
46
+ - Fix the private key range check.
47
+
48
+ ## [0.2.2] - 2023-01-26
49
+
50
+ - Add `BLS::Field#==` and `BLS::Field#to_s`.
51
+
52
+ ## [0.2.1] - 2023-01-16
53
+
54
+ - Add argument validation and error message cleanup.
55
+
56
+ ## [0.2.0] - 2022-09-07
57
+
58
+ - Add `BLS::PointG2.from_private_key`.
59
+
3
60
  ## [0.1.0] - 2021-03-29
4
61
 
5
62
  - Initial release
data/README.md CHANGED
@@ -27,8 +27,8 @@ Or install it yourself as:
27
27
  ```ruby
28
28
  require 'bls'
29
29
 
30
- # Generate private key.
31
- private_key = SecureRandom.random_number(BLS::Curve::R - 1)
30
+ # Generate private key: an integer in 1..(R - 1). Zero is not a key, so shift the range up.
31
+ private_key = 1 + SecureRandom.random_number(BLS::Curve::R - 1)
32
32
 
33
33
  # Or you can use hex string.
34
34
  private_key = '67d53f170b908cabb9eb326c3c337762d59289a8fec79f7bc9254b584b73265c'
@@ -44,7 +44,7 @@ message = '64726e3da8'
44
44
  signature = BLS.sign(message, private_key)
45
45
  # signature is BLS::PointG2 object. You can get signature with hex format using #to_hex method.
46
46
  # If you want to use BLS::PointG1 signature, use BLS.sign(message, p, sig_type: :g1)
47
- signature.to_signature
47
+ signature.to_hex(compressed: true)
48
48
 
49
49
  is_correct = BLS.verify(signature, message, public_key)
50
50
  => true
@@ -119,11 +119,13 @@ end
119
119
  Passing the wrong type or the wrong number of coefficients is a mistake in the calling code
120
120
  rather than a fact about a key, and raises `ArgumentError` as it would anywhere else in Ruby.
121
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.
122
+ Verification does not raise over the keys and signatures it is given. `BLS.verify`,
123
+ `BLS.verify_batch`, `BLS.fast_aggregate_verify` and `BLS.pop_verify` return `false` for any
124
+ point they will not accept, including the point at infinity and an empty set of signers, so an
125
+ untrusted key or signature cannot turn into an exception. Deserializing it with `from_hex`
126
+ beforehand still raises, which is where a malformed encoding is caught. The message is
127
+ different: it is the hex of a hash the caller computed, and one that is not hex, or has an odd
128
+ number of digits, raises `BLS::PointError` from signing and verifying alike.
127
129
 
128
130
  ## Threads
129
131
 
@@ -164,4 +166,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
164
166
 
165
167
  ## Code of Conduct
166
168
 
167
- Everyone interacting in the BlS12-381 project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/bls12-381/blob/master/CODE_OF_CONDUCT.md).
169
+ Everyone interacting in the BLS12-381 project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/azuchi/bls12-381/blob/main/CODE_OF_CONDUCT.md).
data/bls12-381.gemspec CHANGED
@@ -12,7 +12,7 @@ Gem::Specification.new do |spec|
12
12
  spec.description = 'BLS12-381 implementation for Ruby.'
13
13
  spec.homepage = 'https://github.com/azuchi/bls12-381'
14
14
  spec.license = 'MIT'
15
- spec.required_ruby_version = Gem::Requirement.new('>= 2.4.0')
15
+ spec.required_ruby_version = Gem::Requirement.new('>= 3.3.0')
16
16
 
17
17
  spec.metadata['homepage_uri'] = spec.homepage
18
18
  spec.metadata['source_code_uri'] = spec.homepage
@@ -27,8 +27,7 @@ Gem::Specification.new do |spec|
27
27
  spec.require_paths = ['lib']
28
28
 
29
29
  # Uncomment to register a new dependency of your gem
30
- spec.add_dependency "h2c", ">= 0.2.1", "< 0.3"
31
- spec.add_dependency "bigdecimal"
30
+ spec.add_dependency "h2c", "~> 0.4.0"
32
31
 
33
32
  spec.add_development_dependency 'bundler'
34
33
  spec.add_development_dependency 'rake', '>= 12.3.3'
data/lib/bls/field.rb CHANGED
@@ -45,7 +45,10 @@ module BLS
45
45
  self.class.new(value**2)
46
46
  end
47
47
 
48
+ # @raise [ArgumentError] Occur when +n+ is negative. Divide by the positive power instead.
48
49
  def pow(n)
50
+ raise ArgumentError, 'Exponent must not be negative.' if n.negative?
51
+
49
52
  v = value.pow(n, self.class.const_get(:ORDER))
50
53
  self.class.new(v)
51
54
  end
@@ -130,6 +133,8 @@ module BLS
130
133
  module FQP
131
134
 
132
135
  def ==(other)
136
+ return false unless other.is_a?(self.class)
137
+
133
138
  coeffs == other.coeffs
134
139
  end
135
140
 
@@ -157,7 +162,11 @@ module BLS
157
162
  self.class.new(coeffs.map(&:negate))
158
163
  end
159
164
 
165
+ # @raise [ArgumentError] Occur when +n+ is negative, which the loop below would otherwise
166
+ # answer with ONE as though it had been zero.
160
167
  def pow(n)
168
+ raise ArgumentError, 'Exponent must not be negative.' if n.negative?
169
+
161
170
  one = self.class.const_get(:ONE)
162
171
  return one if n.zero?
163
172
  return self if n == 1
data/lib/bls/h2c.rb CHANGED
@@ -3,18 +3,28 @@ module BLS
3
3
 
4
4
  LENGTH = 64
5
5
 
6
+ # Longest tag expand_message_xmd can carry as is; its length has to fit in one byte.
7
+ MAX_DST_LENGTH = 255
8
+ # Prefix under which a longer tag is hashed down to size, from RFC 9380 section 5.3.3.
9
+ OVERSIZE_DST_PREFIX = 'H2C-OVERSIZE-DST-'
10
+
6
11
  module_function
7
12
 
8
13
  # @param [String] message hash value with hex format.
9
14
  # @param [Integer] len_in_bytes length
10
15
  # @param [String] dst Domain separation tag. Mixing tags across schemes breaks the
11
16
  # domain separation the ciphersuites rely on, so it must always be passed explicitly.
17
+ # A tag longer than {MAX_DST_LENGTH} bytes is replaced by its hash, as RFC 9380 section
18
+ # 5.3.3 prescribes, so any length works and the result matches other implementations.
12
19
  # @return [Array[Integer]] byte array.
13
20
  # @raise BLS::Error
14
21
  def expand_message_xmd(message, len_in_bytes, dst)
15
- b_in_bytes = BigDecimal(SHA256_DIGEST_SIZE)
22
+ raise BLS::Error, 'Domain separation tag must not be empty.' if dst.empty? # RFC 9380 section 3.1
23
+
24
+ dst = Digest::SHA256.digest(OVERSIZE_DST_PREFIX + dst) if dst.bytesize > MAX_DST_LENGTH
25
+ b_in_bytes = SHA256_DIGEST_SIZE
16
26
  r_in_bytes = b_in_bytes * 2
17
- ell = (BigDecimal(len_in_bytes) / b_in_bytes).ceil
27
+ ell = (len_in_bytes + b_in_bytes - 1) / b_in_bytes
18
28
  raise BLS::Error, 'Invalid xmd length' if ell > 255
19
29
 
20
30
  dst_prime = dst.bytes + BLS.i2osp(dst.bytesize, 1)
@@ -24,7 +34,10 @@ module BLS
24
34
  payload = z_pad + [message].pack('H*').bytes + l_i_b_str + BLS.i2osp(0, 1) + dst_prime
25
35
  b_0 = Digest::SHA256.digest(payload.pack('C*'))
26
36
  b[0] = Digest::SHA256.digest((b_0.bytes + BLS.i2osp(1, 1) + dst_prime).pack('C*'))
27
- (1..ell).each do |i|
37
+ # b[i] holds the RFC's b_(i + 1), so b_2 .. b_ell is ell - 1 more blocks. Running the
38
+ # loop to ell made one block too many: harmless after the truncation below, except that
39
+ # at ell == 255 its counter byte overflowed and the call failed at the RFC's own limit.
40
+ (1...ell).each do |i|
28
41
  args = BLS.bin_xor(b_0, b[i - 1]).bytes + BLS.i2osp(i + 1, 1) + dst_prime
29
42
  b[i] = Digest::SHA256.digest(args.pack('C*'))
30
43
  end
@@ -92,7 +105,7 @@ module BLS
92
105
  end
93
106
 
94
107
  # 3-isogeny map from E' to E
95
- # Converts from Jacobi (xyz) to Projective (xyz) coordinates.
108
+ # Takes and returns projective (x, y, z) coordinates.
96
109
  def isogeny_map(x, y, z)
97
110
  mapped = Array.new(4, Fp2::ZERO)
98
111
  z_powers = [z, z**2, z**3]
data/lib/bls/pairing.rb CHANGED
@@ -6,6 +6,9 @@ module BLS
6
6
  # @param [Boolean] with_final_exp
7
7
  # @return [BLS::Fp12]
8
8
  # @raise [BLS::PairingError] Occur when p.zero? or q.zero?
9
+ # @raise [BLS::PointError] Occur when either point is off its curve or outside its
10
+ # prime-order subgroup. from_hex has already checked what it returns; this is for points
11
+ # built by hand, for which the pairing would otherwise be computed without complaint.
9
12
  # @raise [ArgumentError]
10
13
  def pairing(p, q, with_final_exp: true)
11
14
  raise ArgumentError, 'p should be BLS::PointG1 object' unless p.is_a?(BLS::PointG1)
@@ -14,6 +17,8 @@ module BLS
14
17
 
15
18
  p.validate!
16
19
  q.validate!
20
+ p.validate_group!
21
+ q.validate_group!
17
22
  looped = p.miller_loop(q)
18
23
  with_final_exp ? looped.final_exponentiate : looped
19
24
  end
data/lib/bls/point/g1.rb CHANGED
@@ -11,7 +11,6 @@ module BLS
11
11
 
12
12
  BASE = PointG1.new(Fp.new(Curve::G_X), Fp.new(Curve::G_Y), Fp::ONE)
13
13
  ZERO = PointG1.new(Fp::ONE, Fp::ONE, Fp::ZERO)
14
- MAX_BITS = Fp::MAX_BITS
15
14
 
16
15
  # Parse PointG1 from form hex.
17
16
  # @param [String] hex hex value of PointG1.
@@ -106,10 +105,30 @@ module BLS
106
105
  def self.hash_to_curve(message, scheme: :basic, dst: nil)
107
106
  validate_hex!(message)
108
107
 
109
- h2c = ::H2C.get(::H2C::Suite::BLS12381G1_XMDSHA256_SSWU_RO_, dst || self.dst(scheme))
108
+ h2c = ::H2C.get(::H2C::Suite::BLS12381G1_XMDSHA256_SSWU_RO_, resolve_dst(dst, scheme))
110
109
  p = h2c.digest([message].pack('H*'))
111
110
 
112
111
  PointG1.new(Fp.new(p.x), Fp.new(p.y), Fp::ONE)
112
+ rescue ::H2C::Error => e
113
+ # Whatever the h2c gem objects to, callers rescue it the way they rescue the rest.
114
+ raise PointError, e.message
115
+ end
116
+
117
+ # A primitive cube root of unity in Fp. (x, y) -> (BETA * x, y) is an endomorphism of E(Fp)
118
+ # that acts on G1 as multiplication by -z**2, z being the curve parameter. Of the two
119
+ # primitive roots, this is the one for which that holds; the other gives the inverse map.
120
+ ENDO_BETA = Fp.new(PSI2_C1).square
121
+
122
+ # Check whether this point belongs to G1, by the test of section 4 of
123
+ # https://eprint.iacr.org/2021/1130: a point of E(Fp) satisfies phi(P) == [-z**2]P exactly
124
+ # when it lies in G1. Two multiplications by the 64-bit z, with its six set bits, instead
125
+ # of one by the 255-bit r.
126
+ # @return [Boolean] true if this point is in G1.
127
+ def in_group?
128
+ return true if zero?
129
+
130
+ endo = PointG1.new(x * ENDO_BETA, y, z)
131
+ endo == multiply_unsafe(Curve::X).multiply_unsafe(Curve::X).negate
113
132
  end
114
133
 
115
134
  # Validate this point whether on curve over Fp.
data/lib/bls/point/g2.rb CHANGED
@@ -11,7 +11,6 @@ module BLS
11
11
  KEY_SIZE_COMPRESSED = 96
12
12
  KEY_SIZE_UNCOMPRESSED = 192
13
13
 
14
- MAX_BITS = Fp2::MAX_BITS
15
14
  BASE = PointG2.new(Fp2.new(Curve::G2_X), Fp2.new(Curve::G2_Y), Fp2::ONE)
16
15
  ZERO = PointG2.new(Fp2::ONE, Fp2::ONE, Fp2::ZERO)
17
16
 
@@ -45,6 +44,11 @@ module BLS
45
44
  return ZERO if i_bit == POINT_INFINITY_FLAG
46
45
  x1 = bytes[0...PUBLIC_KEY_LENGTH].unpack1('H*').to_i(16)
47
46
  x0 = bytes[PUBLIC_KEY_LENGTH...(2 * PUBLIC_KEY_LENGTH)].unpack1('H*').to_i(16)
47
+ # Fp.new reduces modulo P, so without this check x + P would decode as the same
48
+ # point under a second encoding. G1 refuses these too.
49
+ unless x0 < Curve::P && x1 < Curve::P
50
+ raise PointError, 'Invalid point G2, coordinates must be less than the field order.'
51
+ end
48
52
  x = Fp2.new([x0, x1])
49
53
  right = x ** 3 + Fp2.new(Curve::B2)
50
54
  y = right.sqrt
@@ -66,6 +70,9 @@ module BLS
66
70
  x0 = bytes[PUBLIC_KEY_LENGTH...(2 * PUBLIC_KEY_LENGTH)].unpack1('H*').to_i(16)
67
71
  y1 = bytes[(2 * PUBLIC_KEY_LENGTH)...(3 * PUBLIC_KEY_LENGTH)].unpack1('H*').to_i(16)
68
72
  y0 = bytes[(3 * PUBLIC_KEY_LENGTH)..-1].unpack1('H*').to_i(16)
73
+ unless [x0, x1, y0, y1].all? { |c| c < Curve::P }
74
+ raise PointError, 'Invalid point G2, coordinates must be less than the field order.'
75
+ end
69
76
  PointG2.new(Fp2.new([x0, x1]), Fp2.new([y0, y1]), Fp2::ONE)
70
77
  else
71
78
  raise PointError, 'Invalid point G2, compression flag does not match the encoded length.'
@@ -93,7 +100,7 @@ module BLS
93
100
  def self.hash_to_curve(message, scheme: :basic, dst: nil)
94
101
  validate_hex!(message)
95
102
 
96
- u = BLS::H2C::G2.hash_to_field(message, dst || self.dst(scheme))
103
+ u = BLS::H2C::G2.hash_to_field(message, resolve_dst(dst, scheme))
97
104
  q0 = PointG2.new(*BLS::H2C::G2.isogeny_map(*BLS::H2C::G2.map_to_curve_sswu(u[0])))
98
105
  q1 = PointG2.new(*BLS::H2C::G2.isogeny_map(*BLS::H2C::G2.map_to_curve_sswu(u[1])))
99
106
  r = q0 + q1
@@ -140,6 +147,20 @@ module BLS
140
147
  to_hex(compressed: true)
141
148
  end
142
149
 
150
+ # Check whether this point belongs to G2, by the test of section 4 of
151
+ # https://eprint.iacr.org/2021/1130: psi, the untwist-Frobenius-twist endomorphism that
152
+ # {clear_cofactor} also uses, acts on G2 as multiplication by the curve parameter z, and a
153
+ # point of E'(Fp2) satisfies psi(P) == [z]P exactly when it lies in G2. z is negative for
154
+ # BLS12-381 and Curve::X holds its magnitude, hence the negate. One multiplication by a
155
+ # 64-bit scalar with six set bits instead of one by the 255-bit r.
156
+ # @return [Boolean] true if this point is in G2.
157
+ def in_group?
158
+ return true if zero?
159
+
160
+ psi = from_affine_tuple(BLS.psi(*to_affine))
161
+ psi == multiply_unsafe(Curve::X).negate
162
+ end
163
+
143
164
  def validate!
144
165
  b = Fp2.new(Curve::B2)
145
166
  return if zero?
data/lib/bls/point.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'bigdecimal'
4
3
  require 'h2c'
5
4
 
6
5
  module BLS
@@ -16,6 +15,11 @@ module BLS
16
15
  # Abstract Point class that consist of projective coordinates.
17
16
  class ProjectivePoint
18
17
 
18
+ # Bits in the group order r. Every point this library hands out lies in the subgroup of
19
+ # that order, where [n]P == [n mod r]P, so this, not the size of the base field, is what
20
+ # a scalar multiplication has to cover.
21
+ SCALAR_BITS = Curve::R.bit_length
22
+
19
23
  attr_reader :x, :y, :z
20
24
  attr_accessor :m_precomputes
21
25
 
@@ -40,6 +44,22 @@ module BLS
40
44
  end
41
45
  end
42
46
 
47
+ # The tag {hash_to_curve} will use: the +dst+ given, or the one +scheme+ names.
48
+ # An empty tag is refused here, under BLS::Error, for both groups alike. RFC 9380 section
49
+ # 3.1 forbids it, and left to the layers below it would either go through unnoticed (G2)
50
+ # or surface as the h2c gem's own exception (G1), outside this library's hierarchy.
51
+ # @param [String, nil] dst explicit domain separation tag, if any.
52
+ # @param [Symbol] scheme signature scheme to take the tag from otherwise.
53
+ # @return [String] domain separation tag.
54
+ # @raise [BLS::Error] Occur when the tag is empty or not a string, or the scheme unknown.
55
+ def self.resolve_dst(dst, scheme)
56
+ return self.dst(scheme) if dst.nil?
57
+ raise BLS::Error, 'Domain separation tag must be a string.' unless dst.is_a?(String)
58
+ raise BLS::Error, 'Domain separation tag must not be empty.' if dst.empty?
59
+
60
+ dst
61
+ end
62
+
43
63
  # Check that +hex+ is a whole number of bytes written in hex, for {from_hex} and
44
64
  # {hash_to_curve}, which both unpack their argument with pack('H*').
45
65
  #
@@ -73,11 +93,13 @@ module BLS
73
93
  self.class.new(x, y, z)
74
94
  end
75
95
 
76
- # Compare one point to another.
96
+ # Compare one point to another. A G1 point never equals a G2 point, nil, or anything
97
+ # else that is not a point of the same group, and says so with false rather than an
98
+ # exception, since Array#include?, uniq and the like call this on whatever they hold.
77
99
  # @param [ProjectivePoint] other another point.
78
100
  # @return [Boolean] whether same point or not.
79
101
  def ==(other)
80
- raise PointError, "ProjectivePoint#==: this is #{self.class}, but other is #{other.class}" unless self.class == other.class
102
+ return false unless other.is_a?(self.class)
81
103
 
82
104
  (x * other.z) == (other.x * z) && (y * other.z) == (other.y * z)
83
105
  end
@@ -163,6 +185,9 @@ module BLS
163
185
  # Being on the curve is not sufficient: E(Fp) and E'(Fp2) both contain points
164
186
  # outside the order-r subgroup, and accepting them makes keys and signatures malleable.
165
187
  # Since r**2 does not divide the group order, [r]P == O holds only for P in the subgroup.
188
+ # That is the definition and the reference the specs compare against; PointG1 and PointG2
189
+ # override it with an endomorphism check that reaches the same answer in a fraction of
190
+ # the time.
166
191
  # @return [Boolean] true if this point is in the prime-order subgroup.
167
192
  def in_group?
168
193
  zero? || multiply_unsafe(Curve::R).zero?
@@ -217,24 +242,35 @@ module BLS
217
242
 
218
243
  # Scalar multiplication using wNAF.
219
244
  #
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.
245
+ # This is NOT constant time, despite what {multiply_unsafe} implies by contrast. The
246
+ # number of additions follows the number of non-zero windows in the scalar, the point
247
+ # each one adds is read at an index derived from the scalar, and the additions themselves
248
+ # branch on whether an operand is the identity or the two are equal. Underneath, Ruby's
249
+ # bignum arithmetic and the modulo in Fp both run in time that depends on their operands.
250
+ # Anything the scalar decides is therefore visible to something watching timing or cache
251
+ # behaviour, which matters here because #sign and .from_private_key reach this with the
252
+ # private key. See the README.
253
+ #
254
+ # noble, which this follows, adds a throwaway point on every zero window so that each
255
+ # window costs one addition whatever its digit. That buys nothing here, where the
256
+ # arithmetic underneath is not constant time to begin with, so the zero windows are
257
+ # skipped and the work saved instead.
227
258
  def multiply(scalar)
228
259
  n = scalar.is_a?(Field) ? scalar.value : scalar
229
260
  raise PointError, 'Invalid scalar, expected positive integer' if n <= 0
230
- raise PointError, "Scalar has more bits than maxBits, shouldn't happen" if n.bit_length > max_bits
231
261
 
232
- wNAF(n).first
262
+ # Reduced modulo r so that wNAF has {SCALAR_BITS} bits to walk, whatever was passed in.
263
+ # The table used to be sized from the base field instead, 381 bits for G1 and 762 for
264
+ # G2, and every window past the scalar's top bit was a wasted addition.
265
+ n %= Curve::R
266
+ return zero if n.zero?
267
+
268
+ wNAF(n)
233
269
  end
234
270
  alias * multiply
235
271
 
236
272
  def precomputes_window(w)
237
- windows = (BigDecimal(max_bits) / w).ceil
273
+ windows = window_count(w)
238
274
  window_size = 2**(w - 1)
239
275
  points = []
240
276
  p = self
@@ -250,8 +286,17 @@ module BLS
250
286
  points
251
287
  end
252
288
 
253
- def max_bits
254
- self.class.const_get(:MAX_BITS)
289
+ # Number of width-+w+ windows that {wNAF} walks and {precomputes_window} must cover.
290
+ #
291
+ # One bit more than the scalar has. A window whose digit comes out above 2**(w - 1) is
292
+ # recoded as a negative digit and carries 1 into the next window, and when the top
293
+ # window of the scalar does that, the carry needs a window of its own or it is dropped
294
+ # on the floor. With r's top five bits being 11100, a width of 5 hits this for about
295
+ # four scalars in ten.
296
+ # @param [Integer] w window width.
297
+ # @return [Integer] window count.
298
+ def window_count(w)
299
+ (SCALAR_BITS + w) / w
255
300
  end
256
301
 
257
302
  def normalize_z(points)
@@ -285,8 +330,7 @@ module BLS
285
330
  def wNAF(n)
286
331
  w, precomputes = m_precomputes || [1, precomputes_window(1)]
287
332
  p = zero
288
- f = zero
289
- windows = (BigDecimal(max_bits) / w).ceil
333
+ windows = window_count(w)
290
334
  window_size = 2**(w - 1)
291
335
  mask = (2**w - 1)
292
336
  max_number = 2**w
@@ -299,27 +343,17 @@ module BLS
299
343
  wbits -= max_number
300
344
  n += 1
301
345
  end
302
- if wbits.zero?
303
- f += (window % 2 ? precomputes[offset].negate : precomputes[offset])
304
- else
305
- cached = precomputes[offset + wbits.abs - 1]
306
- p += (wbits.negative? ? cached.negate : cached)
307
- end
346
+ next if wbits.zero?
347
+
348
+ cached = precomputes[offset + wbits.abs - 1]
349
+ p += (wbits.negative? ? cached.negate : cached)
308
350
  end
309
- [p, f]
351
+ p
310
352
  end
311
353
  end
312
354
 
313
355
  module_function
314
356
 
315
- def norm_p1(point)
316
- point.is_a?(PointG1) ? point : PointG1.from_hex(point)
317
- end
318
-
319
- def norm_p2(point)
320
- point.is_a?(PointG2) ? point : PointG2.from_hex(point)
321
- end
322
-
323
357
  def norm_p1h(point, scheme: :basic)
324
358
  point.is_a?(PointG1) ? point : PointG1.hash_to_curve(point, scheme: scheme)
325
359
  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.4.0'
4
+ VERSION = '0.5.0'
5
5
  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.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shigeyuki Azuchi
@@ -13,36 +13,16 @@ dependencies:
13
13
  name: h2c
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
- - - ">="
17
- - !ruby/object:Gem::Version
18
- version: 0.2.1
19
- - - "<"
16
+ - - "~>"
20
17
  - !ruby/object:Gem::Version
21
- version: '0.3'
18
+ version: 0.4.0
22
19
  type: :runtime
23
20
  prerelease: false
24
21
  version_requirements: !ruby/object:Gem::Requirement
25
22
  requirements:
26
- - - ">="
27
- - !ruby/object:Gem::Version
28
- version: 0.2.1
29
- - - "<"
30
- - !ruby/object:Gem::Version
31
- version: '0.3'
32
- - !ruby/object:Gem::Dependency
33
- name: bigdecimal
34
- requirement: !ruby/object:Gem::Requirement
35
- requirements:
36
- - - ">="
23
+ - - "~>"
37
24
  - !ruby/object:Gem::Version
38
- version: '0'
39
- type: :runtime
40
- prerelease: false
41
- version_requirements: !ruby/object:Gem::Requirement
42
- requirements:
43
- - - ">="
44
- - !ruby/object:Gem::Version
45
- version: '0'
25
+ version: 0.4.0
46
26
  - !ruby/object:Gem::Dependency
47
27
  name: bundler
48
28
  requirement: !ruby/object:Gem::Requirement
@@ -115,7 +95,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
115
95
  requirements:
116
96
  - - ">="
117
97
  - !ruby/object:Gem::Version
118
- version: 2.4.0
98
+ version: 3.3.0
119
99
  required_rubygems_version: !ruby/object:Gem::Requirement
120
100
  requirements:
121
101
  - - ">="