bls12-381 0.3.1 → 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: 2660bb6049da204d945d3d40a6bf06f26ed1c041794d959ea9b0662b725a1b8b
4
- data.tar.gz: ad30c4cc593a729823bd51d05a1bdecc68566ed0e5b100cd31dc8130bf6e1353
3
+ metadata.gz: 24158dde28a5367b3fd19c5d77dfeb70ba7429c43f501f1e15345dc5792cfcd2
4
+ data.tar.gz: ee9dd866975f45538e1bd4cfa044fd88741cd0e01176fc6dea075111eab88cfe
5
5
  SHA512:
6
- metadata.gz: eab97d8693ad91b412a1f7c8dd362b0b82c241e96c220ba686e79208c0828b3cd27303ecc8fa68ae51e18ca1f596d87bceff9b35ab009795e438784b8cab66a6
7
- data.tar.gz: f1672d9beb73fa230f0d1bfe1b2ef56955599ed79d46ebd9a98cf344f593f705e6a63351dd87c7e65b8de25867705e915ae3f04d8e047ab9bd01741219dd5345
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
@@ -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
 
@@ -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,23 +44,17 @@ 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
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,12 +62,108 @@ 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 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.
129
+
130
+ ## Threads
131
+
132
+ Signing and verifying from several threads gives the right answers, but two caches are
133
+ written without a lock and neither is worth sharing a point over.
134
+
135
+ `calc_multiply_precomputes` builds its table over long enough that MRI switches threads part
136
+ way through, so callers racing on one point all pass the "already has precomputes" check and
137
+ each build their own. They build the same table, so the result stays correct; what you lose is
138
+ the time and the memory of doing it several times over, and a guard that reads as protection
139
+ while providing none. Build the table once, before the point goes anywhere else.
140
+
141
+ `BLS.verify` memoises pairing coefficients onto the G2 point it is given, and onto
142
+ `BLS::PointG2::BASE` when the public key is a G2 point. Racing callers recompute rather than
143
+ corrupt, for the same reason: what is assigned is always a finished array.
144
+
145
+ ## Side channels
146
+
147
+ **Nothing here is constant time, and signing is not safe against an attacker who can
148
+ measure it.** `BLS.sign` and `BLS.get_public_key` multiply a curve point by the private
149
+ key, and that multiplication leaks:
150
+
151
+ * the scalar picks which precomputed point each window adds, so it decides which memory
152
+ gets touched, which is visible through the cache;
153
+ * point addition returns early when an operand is the identity or the two are equal;
154
+ * underneath it all, Ruby's bignum arithmetic and the `%` in `BLS::Fp` take time that
155
+ depends on their operands.
156
+
157
+ A pure Ruby implementation cannot fix this: it has no control over how the interpreter
158
+ lays out or times its integers. Treat this gem as suitable where the private key operation
159
+ cannot be observed — verification of untrusted input is fine, since it touches no secret —
160
+ and use a binding to a constant time library such as [blst](https://github.com/supranational/blst)
161
+ where an attacker can watch the signer.
162
+
73
163
  ## License
74
164
 
75
165
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
76
166
 
77
167
  ## Code of Conduct
78
168
 
79
- 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/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
@@ -37,7 +45,10 @@ module BLS
37
45
  self.class.new(value**2)
38
46
  end
39
47
 
48
+ # @raise [ArgumentError] Occur when +n+ is negative. Divide by the positive power instead.
40
49
  def pow(n)
50
+ raise ArgumentError, 'Exponent must not be negative.' if n.negative?
51
+
41
52
  v = value.pow(n, self.class.const_get(:ORDER))
42
53
  self.class.new(v)
43
54
  end
@@ -122,6 +133,8 @@ module BLS
122
133
  module FQP
123
134
 
124
135
  def ==(other)
136
+ return false unless other.is_a?(self.class)
137
+
125
138
  coeffs == other.coeffs
126
139
  end
127
140
 
@@ -149,7 +162,11 @@ module BLS
149
162
  self.class.new(coeffs.map(&:negate))
150
163
  end
151
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.
152
167
  def pow(n)
168
+ raise ArgumentError, 'Exponent must not be negative.' if n.negative?
169
+
153
170
  one = self.class.const_get(:ONE)
154
171
  return one if n.zero?
155
172
  return self if n == 1
@@ -248,7 +265,15 @@ module BLS
248
265
  Fp2.new([a * b, c * c1])
249
266
  end
250
267
 
268
+ # Square root, or nil when this element is not a square.
269
+ # Zero is answered up front. It is a square, its root being itself, and the division below
270
+ # would otherwise be asked for the inverse of zero. That used to yield zero and land here
271
+ # as a nil, which reads as "not a square" and is wrong for the one element that has
272
+ # exactly one root.
273
+ # @return [BLS::Fp2, nil]
251
274
  def sqrt
275
+ return self if zero?
276
+
252
277
  candidate = pow((Fp2::ORDER + 8) / 16)
253
278
  check = candidate.square / self
254
279
  r = ROOTS_OF_UNITY
@@ -700,12 +725,17 @@ module BLS
700
725
 
701
726
 
702
727
 
728
+ # sgn0 for GF(p^2), from RFC 9380 section 4.1.
729
+ # Combined with | and & rather than || and &&: sign_0 is 0 or 1, and 0 is truthy in Ruby,
730
+ # so a disjunction would always short circuit on it and x_1 would never be reached.
731
+ # @param [BLS::Fp2] x
732
+ # @return [Integer] 0 or 1.
703
733
  def sgn0(x)
704
734
  x0, x1 = x.values
705
735
  sign_0 = x0 % 2
706
- zero_0 = x0 === 0
736
+ zero_0 = x0.zero? ? 1 : 0
707
737
  sign_1 = x1 % 2
708
- sign_0 || (zero_0 && sign_1)
738
+ sign_0 | (zero_0 & sign_1)
709
739
  end
710
740
 
711
741
  def sqrt_div_fp2(u, v)
data/lib/bls/h2c.rb CHANGED
@@ -3,26 +3,41 @@ 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
15
+ # @param [String] dst Domain separation tag. Mixing tags across schemes breaks the
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.
10
19
  # @return [Array[Integer]] byte array.
11
20
  # @raise BLS::Error
12
- def expand_message_xmd(message, len_in_bytes)
13
- b_in_bytes = BigDecimal(SHA256_DIGEST_SIZE)
21
+ def expand_message_xmd(message, len_in_bytes, dst)
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
14
26
  r_in_bytes = b_in_bytes * 2
15
- ell = (BigDecimal(len_in_bytes) / b_in_bytes).ceil
27
+ ell = (len_in_bytes + b_in_bytes - 1) / b_in_bytes
16
28
  raise BLS::Error, 'Invalid xmd length' if ell > 255
17
29
 
18
- dst_prime = PointG2::DST_BASIC.bytes + BLS.i2osp(PointG2::DST_BASIC.bytesize, 1)
30
+ dst_prime = dst.bytes + BLS.i2osp(dst.bytesize, 1)
19
31
  z_pad = BLS.i2osp(0, r_in_bytes)
20
32
  l_i_b_str = BLS.i2osp(len_in_bytes, 2)
21
33
  b = Array.new(ell)
22
34
  payload = z_pad + [message].pack('H*').bytes + l_i_b_str + BLS.i2osp(0, 1) + dst_prime
23
35
  b_0 = Digest::SHA256.digest(payload.pack('C*'))
24
36
  b[0] = Digest::SHA256.digest((b_0.bytes + BLS.i2osp(1, 1) + dst_prime).pack('C*'))
25
- (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|
26
41
  args = BLS.bin_xor(b_0, b[i - 1]).bytes + BLS.i2osp(i + 1, 1) + dst_prime
27
42
  b[i] = Digest::SHA256.digest(args.pack('C*'))
28
43
  end
@@ -34,12 +49,13 @@ module BLS
34
49
 
35
50
  # Convert hash to Field.
36
51
  # @param [String] message hash value with hex format.
52
+ # @param [String] dst Domain separation tag of the ciphersuite in use.
37
53
  # @return [Array[Integer]] byte array.
38
- def hash_to_field(message, random_oracle: true)
54
+ def hash_to_field(message, dst = PointG2::DST_BASIC, random_oracle: true)
39
55
  degree = 2
40
56
  count = random_oracle ? 2 : 1
41
57
  len_in_bytes = count * degree * LENGTH
42
- pseudo_random_bytes = BLS::H2C.expand_message_xmd(message, len_in_bytes)
58
+ pseudo_random_bytes = BLS::H2C.expand_message_xmd(message, len_in_bytes, dst)
43
59
  u = Array.new(count)
44
60
  count.times do |i|
45
61
  e = Array.new(degree)
@@ -89,7 +105,7 @@ module BLS
89
105
  end
90
106
 
91
107
  # 3-isogeny map from E' to E
92
- # Converts from Jacobi (xyz) to Projective (xyz) coordinates.
108
+ # Takes and returns projective (x, y, z) coordinates.
93
109
  def isogeny_map(x, y, z)
94
110
  mapped = Array.new(4, Fp2::ZERO)
95
111
  z_powers = [z, z**2, z**3]
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,13 +1,14 @@
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
9
7
  # @return [BLS::Fp12]
10
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.
11
12
  # @raise [ArgumentError]
12
13
  def pairing(p, q, with_final_exp: true)
13
14
  raise ArgumentError, 'p should be BLS::PointG1 object' unless p.is_a?(BLS::PointG1)
@@ -16,8 +17,25 @@ module BLS
16
17
 
17
18
  p.validate!
18
19
  q.validate!
20
+ p.validate_group!
21
+ q.validate_group!
19
22
  looped = p.miller_loop(q)
20
23
  with_final_exp ? looped.final_exponentiate : looped
21
24
  end
22
25
 
26
+ # Pairing without the final exponentiation, using e(O, Q) = e(P, O) = 1 where {pairing}
27
+ # raises instead. Verification multiplies several of these together, so a degenerate key
28
+ # or signature has to fall out as a failed check rather than as an exception.
29
+ # @param [BLS::PointG1] p
30
+ # @param [BLS::PointG2] q
31
+ # @return [BLS::Fp12]
32
+ # @raise [ArgumentError]
33
+ def partial_pairing(p, q)
34
+ raise ArgumentError, 'p should be BLS::PointG1 object' unless p.is_a?(BLS::PointG1)
35
+ raise ArgumentError, 'q should be BLS::PointG2 object' unless q.is_a?(BLS::PointG2)
36
+ return Fp12::ONE if p.zero? || q.zero?
37
+
38
+ pairing(p, q, with_final_exp: false)
39
+ end
40
+
23
41
  end
data/lib/bls/point/g1.rb CHANGED
@@ -1,45 +1,69 @@
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
8
11
 
9
12
  BASE = PointG1.new(Fp.new(Curve::G_X), Fp.new(Curve::G_Y), Fp::ONE)
10
13
  ZERO = PointG1.new(Fp::ONE, Fp::ONE, Fp::ZERO)
11
- MAX_BITS = Fp::MAX_BITS
12
14
 
13
15
  # Parse PointG1 from form hex.
14
16
  # @param [String] hex hex value of PointG1.
15
17
  # @return [PointG1]
16
- # @raise [BLS::PointError] Occurs when hex length does not match, or point does not on G1.
18
+ # @raise [BLS::PointError] Occurs when hex length does not match, or point is not on G1.
17
19
  def self.from_hex(hex)
20
+ validate_hex!(hex)
18
21
  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)
22
+ unless [KEY_SIZE_COMPRESSED, KEY_SIZE_UNCOMPRESSED].include?(bytes.bytesize)
23
+ raise PointError, 'Invalid point G1, expected 48 or 96 bytes.'
24
+ end
25
+
26
+ m_byte = bytes[0].unpack1('C') & 0xe0
27
+ if [0x20, 0x60, 0xe0].include?(m_byte)
28
+ raise PointError, "Invalid encoding flag: #{m_byte.to_s(16)}"
29
+ end
30
+
31
+ c_bit = m_byte & POINT_COMPRESSION_FLAG # compression flag
32
+ i_bit = m_byte & POINT_INFINITY_FLAG # infinity flag
33
+ s_bit = m_byte & POINT_Y_FLAG # y coordinate sign flag
34
+ bytes[0] = [bytes[0].unpack1('C') & 0x1f].pack('C') # set flag to 0
35
+
36
+ if i_bit == POINT_INFINITY_FLAG && bytes.unpack1('H*').to_i(16) > 0
37
+ raise PointError, 'Invalid point, infinity point should be all 0.'
38
+ end
39
+
40
+ point = if bytes.bytesize == KEY_SIZE_COMPRESSED && c_bit == POINT_COMPRESSION_FLAG # compressed format
41
+ return ZERO if i_bit == POINT_INFINITY_FLAG
42
+
43
+ x = bytes.unpack1('H*').to_i(16)
44
+ raise PointError, 'Invalid point G1, x must be less than the field order.' unless x < Curve::P
45
+
46
+ full_y = BLS.mod(x**3 + Curve::B, Curve::P)
27
47
  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?
48
+ 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
49
 
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
50
+ y = Curve::P - y unless ((y * 2) / Curve::P) == (s_bit.zero? ? 0 : 1)
32
51
  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?
52
+ elsif bytes.bytesize == KEY_SIZE_UNCOMPRESSED && c_bit != POINT_COMPRESSION_FLAG # uncompressed format
53
+ return ZERO if i_bit == POINT_INFINITY_FLAG
35
54
 
36
55
  x = bytes[0...PUBLIC_KEY_LENGTH].unpack1('H*').to_i(16)
37
56
  y = bytes[PUBLIC_KEY_LENGTH..-1].unpack1('H*').to_i(16)
57
+ unless x < Curve::P && y < Curve::P
58
+ raise PointError, 'Invalid point G1, coordinates must be less than the field order.'
59
+ end
60
+
38
61
  PointG1.new(Fp.new(x), Fp.new(y), Fp::ONE)
39
62
  else
40
- raise PointError, 'Invalid point G1, expected 48 or 96 bytes.'
63
+ raise PointError, 'Invalid point G1, compression flag does not match the encoded length.'
41
64
  end
42
65
  point.validate!
66
+ point.validate_group!
43
67
  point
44
68
  end
45
69
 
@@ -73,15 +97,38 @@ module BLS
73
97
 
74
98
  # Convert hash to PointG1
75
99
  # @param [String] message a hash with hex format.
100
+ # @param [Symbol] scheme signature scheme whose domain separation tag is used, :basic or :pop.
101
+ # @param [String] dst a domain separation tag to use instead of the scheme's, for a
102
+ # ciphersuite this library does not name, such as the ones the RFC 9380 vectors use.
76
103
  # @return [BLS::PointG1] point.
77
104
  # @raise [BLS::PointError]
78
- def self.hash_to_curve(message)
79
- raise PointError, 'expected hex string' unless message[/^[a-fA-F0-9]*$/]
105
+ def self.hash_to_curve(message, scheme: :basic, dst: nil)
106
+ validate_hex!(message)
80
107
 
81
- h2c = ::H2C.get(::H2C::Suite::BLS12381G1_XMDSHA256_SSWU_RO_, PointG1::DST_BASIC)
108
+ h2c = ::H2C.get(::H2C::Suite::BLS12381G1_XMDSHA256_SSWU_RO_, resolve_dst(dst, scheme))
82
109
  p = h2c.digest([message].pack('H*'))
83
110
 
84
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
85
132
  end
86
133
 
87
134
  # Validate this point whether on curve over Fp.
data/lib/bls/point/g2.rb CHANGED
@@ -3,12 +3,14 @@ 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
10
13
 
11
- MAX_BITS = Fp2::MAX_BITS
12
14
  BASE = PointG2.new(Fp2.new(Curve::G2_X), Fp2.new(Curve::G2_Y), Fp2::ONE)
13
15
  ZERO = PointG2.new(Fp2::ONE, Fp2::ONE, Fp2::ZERO)
14
16
 
@@ -18,7 +20,12 @@ module BLS
18
20
  # @return [BLS::PointG2] PointG2.
19
21
  # @raise [BLS::PointError]
20
22
  def self.from_hex(hex)
23
+ validate_hex!(hex)
21
24
  bytes = [hex].pack('H*')
25
+ unless [KEY_SIZE_COMPRESSED, KEY_SIZE_UNCOMPRESSED].include?(bytes.bytesize)
26
+ raise PointError, 'Invalid point G2, expected 96/192 bytes.'
27
+ end
28
+
22
29
  m_byte = bytes[0].unpack1('C')& 0xe0
23
30
  if [0x20, 0x60, 0xe0].include?(m_byte)
24
31
  raise PointError, "Invalid encoding flag: #{m_byte.to_s(16)}"
@@ -37,6 +44,11 @@ module BLS
37
44
  return ZERO if i_bit == POINT_INFINITY_FLAG
38
45
  x1 = bytes[0...PUBLIC_KEY_LENGTH].unpack1('H*').to_i(16)
39
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
40
52
  x = Fp2.new([x0, x1])
41
53
  right = x ** 3 + Fp2.new(Curve::B2)
42
54
  y = right.sqrt
@@ -46,7 +58,11 @@ module BLS
46
58
  else
47
59
  (y.coeffs[1].value * 2) / Curve::P == 1 ? 1 : 0
48
60
  end
49
- y = s_bit > 0 && bit_y > 0 ? y : y.negate
61
+ # Flip to whichever root the encoding asked for. Written as a comparison, the
62
+ # way G1 does it, rather than as `s_bit > 0 && bit_y > 0 ? y : y.negate`:
63
+ # that form is only right because sqrt returns the root of sign 1, which it
64
+ # does not promise, and it would negate a root of sign 0 that wanted sign 0.
65
+ y = y.negate unless bit_y == (s_bit.zero? ? 0 : 1)
50
66
  PointG2.new(x, y, Fp2::ONE)
51
67
  elsif bytes.bytesize == KEY_SIZE_UNCOMPRESSED && c_bit != POINT_COMPRESSION_FLAG # uncompressed format
52
68
  return ZERO if i_bit == POINT_INFINITY_FLAG
@@ -54,11 +70,15 @@ module BLS
54
70
  x0 = bytes[PUBLIC_KEY_LENGTH...(2 * PUBLIC_KEY_LENGTH)].unpack1('H*').to_i(16)
55
71
  y1 = bytes[(2 * PUBLIC_KEY_LENGTH)...(3 * PUBLIC_KEY_LENGTH)].unpack1('H*').to_i(16)
56
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
57
76
  PointG2.new(Fp2.new([x0, x1]), Fp2.new([y0, y1]), Fp2::ONE)
58
77
  else
59
- raise PointError, 'Invalid point G2, expected 96/192 bytes.'
78
+ raise PointError, 'Invalid point G2, compression flag does not match the encoded length.'
60
79
  end
61
80
  point.validate!
81
+ point.validate_group!
62
82
  point
63
83
  end
64
84
 
@@ -72,12 +92,15 @@ module BLS
72
92
 
73
93
  # Convert hash to PointG2
74
94
  # @param [String] message a hash with hex format.
95
+ # @param [Symbol] scheme signature scheme whose domain separation tag is used, :basic or :pop.
96
+ # @param [String] dst a domain separation tag to use instead of the scheme's, for a
97
+ # ciphersuite this library does not name, such as the ones the RFC 9380 vectors use.
75
98
  # @return [BLS::PointG2] point.
76
99
  # @raise [BLS::PointError]
77
- def self.hash_to_curve(message)
78
- raise PointError, 'expected hex string' unless message[/^[a-fA-F0-9]*$/]
100
+ def self.hash_to_curve(message, scheme: :basic, dst: nil)
101
+ validate_hex!(message)
79
102
 
80
- u = BLS::H2C::G2.hash_to_field(message)
103
+ u = BLS::H2C::G2.hash_to_field(message, resolve_dst(dst, scheme))
81
104
  q0 = PointG2.new(*BLS::H2C::G2.isogeny_map(*BLS::H2C::G2.map_to_curve_sswu(u[0])))
82
105
  q1 = PointG2.new(*BLS::H2C::G2.isogeny_map(*BLS::H2C::G2.map_to_curve_sswu(u[1])))
83
106
  r = q0 + q1
@@ -124,6 +147,20 @@ module BLS
124
147
  to_hex(compressed: true)
125
148
  end
126
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
+
127
164
  def validate!
128
165
  b = Fp2.new(Curve::B2)
129
166
  return if zero?
@@ -137,6 +174,13 @@ module BLS
137
174
  self.precomputes = nil
138
175
  end
139
176
 
177
+ # The line coefficients {BLS.miller_loop} needs, worked out once and kept on the point.
178
+ #
179
+ # This memoises without a lock, so concurrent callers can each compute it. They compute
180
+ # the same coefficients and assign a finished array, so a reader sees one or the other and
181
+ # never a partial one; the cost is the repeated work. BLS.verify reaches this on
182
+ # PointG2::BASE, which is shared, whenever the public key is a G2 point.
183
+ # @return [Array]
140
184
  def pairing_precomputes
141
185
  return precomputes if precomputes
142
186
 
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
@@ -13,11 +12,14 @@ module BLS
13
12
  autoload :PointG1, "bls/point/g1"
14
13
  autoload :PointG2, "bls/point/g2"
15
14
 
16
- class PointError < StandardError; end
17
-
18
15
  # Abstract Point class that consist of projective coordinates.
19
16
  class ProjectivePoint
20
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
+
21
23
  attr_reader :x, :y, :z
22
24
  attr_accessor :m_precomputes
23
25
 
@@ -28,6 +30,56 @@ module BLS
28
30
  @m_precomputes = nil
29
31
  end
30
32
 
33
+ # Domain separation tag this point class uses for +scheme+.
34
+ # @param [Symbol] scheme :basic, :pop, or :pop_proof(the tag proofs of possession
35
+ # are signed under, kept separate from :pop so a proof cannot pass as a signature).
36
+ # @return [String] domain separation tag.
37
+ # @raise [BLS::Error] Occur when the scheme is unknown.
38
+ def self.dst(scheme)
39
+ case scheme
40
+ when :basic then const_get(:DST_BASIC)
41
+ when :pop then const_get(:DST_POP)
42
+ when :pop_proof then const_get(:DST_POP_PROOF)
43
+ else raise BLS::Error, "Unknown scheme: #{scheme.inspect}. Must be :basic or :pop."
44
+ end
45
+ end
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
+
63
+ # Check that +hex+ is a whole number of bytes written in hex, for {from_hex} and
64
+ # {hash_to_curve}, which both unpack their argument with pack('H*').
65
+ #
66
+ # pack maps a character to a nibble by its low bits, so it reads '3', '#', 'J', 'Z', 'j'
67
+ # and 'z' all as 3 and never complains, and it pads an odd number of digits out to a whole
68
+ # byte. Left alone, that gives every point and every message a large family of spellings
69
+ # that all arrive at the same bytes. Applications tend to carry keys around as hex and
70
+ # compare, deduplicate and index them that way, so the aliases matter as much here as the
71
+ # non-canonical encodings do a layer down.
72
+ #
73
+ # \A..\z rather than ^..$ because those match at line boundaries in Ruby, which would let
74
+ # a newline carry a non-hex tail past the check.
75
+ # @param [String] hex a byte string in hex format.
76
+ # @raise [PointError] Occur when it is not an even length string of hex digits.
77
+ def self.validate_hex!(hex)
78
+ return if hex.is_a?(String) && hex.match?(/\A(?:[0-9a-fA-F]{2})*\z/)
79
+
80
+ raise PointError, 'expected hex string'
81
+ end
82
+
31
83
  def zero?
32
84
  z.zero?
33
85
  end
@@ -41,11 +93,13 @@ module BLS
41
93
  self.class.new(x, y, z)
42
94
  end
43
95
 
44
- # 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.
45
99
  # @param [ProjectivePoint] other another point.
46
100
  # @return [Boolean] whether same point or not.
47
101
  def ==(other)
48
- 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)
49
103
 
50
104
  (x * other.z) == (other.x * z) && (y * other.z) == (other.y * z)
51
105
  end
@@ -109,6 +163,10 @@ module BLS
109
163
  end
110
164
  alias - subtract
111
165
 
166
+ # Scalar multiplication by plain double-and-add, which skips the addition entirely on a
167
+ # zero bit and so takes a number of operations that tracks the scalar's Hamming weight.
168
+ # Only for scalars an observer already knows, such as the curve parameters. {multiply}
169
+ # hides that particular pattern, but is not constant time either.
112
170
  def multiply_unsafe(scalar)
113
171
  n = scalar.is_a?(Field) ? scalar.value : scalar
114
172
  raise PointError, 'Point#multiply: invalid scalar, expected positive integer' if n <= 0
@@ -123,11 +181,33 @@ module BLS
123
181
  p
124
182
  end
125
183
 
184
+ # Check whether this point belongs to the prime-order subgroup (G1 or G2).
185
+ # Being on the curve is not sufficient: E(Fp) and E'(Fp2) both contain points
186
+ # outside the order-r subgroup, and accepting them makes keys and signatures malleable.
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.
191
+ # @return [Boolean] true if this point is in the prime-order subgroup.
192
+ def in_group?
193
+ zero? || multiply_unsafe(Curve::R).zero?
194
+ end
195
+
196
+ # Validate that this point belongs to the prime-order subgroup.
197
+ # @raise [PointError] Occur when this point is not in the prime-order subgroup.
198
+ def validate_group!
199
+ raise PointError, 'Invalid point: not in prime-order subgroup' unless in_group?
200
+ end
201
+
126
202
  def to_affine(inv_z = z.invert)
127
203
  [x * inv_z, y * inv_z]
128
204
  end
129
205
 
206
+ # @raise [PointError] Occur when any of +points+ is the point at infinity, which has no
207
+ # affine representation.
130
208
  def to_affine_batch(points)
209
+ raise PointError, 'The point at infinity has no affine representation.' if points.any?(&:zero?)
210
+
131
211
  to_inv = gen_invert_batch(points.map(&:z))
132
212
  points.map.with_index { |p, i| p.to_affine(to_inv[i]) }
133
213
  end
@@ -136,21 +216,23 @@ module BLS
136
216
  new_point(xy[0], xy[1], x.class.const_get(:ONE))
137
217
  end
138
218
 
219
+ # Inverts a whole array for the price of one inversion plus a few multiplications each.
220
+ # Zero is refused rather than skipped: leaving it in place would hand the caller a zero
221
+ # where it asked for an inverse, and Field#invert refuses it for the same reason.
222
+ # @raise [BLS::Error] Occur when any element is zero.
139
223
  def gen_invert_batch(nums)
224
+ raise BLS::Error, 'Zero has no multiplicative inverse.' if nums.any?(&:zero?)
225
+
140
226
  len = nums.length
141
227
  scratch = Array.new(len)
142
228
  acc = x.class::ONE
143
229
  len.times do |i|
144
- next if nums[i].zero?
145
-
146
230
  scratch[i] = acc
147
231
  acc *= nums[i]
148
232
  end
149
233
  acc = acc.invert
150
234
  len.times do |t|
151
235
  i = len - t - 1
152
- next if nums[i].zero?
153
-
154
236
  tmp = acc * nums[i]
155
237
  nums[i] = acc * scratch[i]
156
238
  acc = tmp
@@ -158,18 +240,37 @@ module BLS
158
240
  nums
159
241
  end
160
242
 
161
- # Constant time multiplication. Uses wNAF.
243
+ # Scalar multiplication using wNAF.
244
+ #
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.
162
258
  def multiply(scalar)
163
259
  n = scalar.is_a?(Field) ? scalar.value : scalar
164
260
  raise PointError, 'Invalid scalar, expected positive integer' if n <= 0
165
- raise PointError, "Scalar has more bits than maxBits, shouldn't happen" if n.bit_length > max_bits
166
261
 
167
- 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)
168
269
  end
169
270
  alias * multiply
170
271
 
171
272
  def precomputes_window(w)
172
- windows = (BigDecimal(max_bits) / w).ceil
273
+ windows = window_count(w)
173
274
  window_size = 2**(w - 1)
174
275
  points = []
175
276
  p = self
@@ -185,14 +286,35 @@ module BLS
185
286
  points
186
287
  end
187
288
 
188
- def max_bits
189
- 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
190
300
  end
191
301
 
192
302
  def normalize_z(points)
193
303
  to_affine_batch(points).map{ |p| from_affine_tuple(p) }
194
304
  end
195
305
 
306
+ # Build the window table that {multiply} then uses, trading memory for speed on a point
307
+ # that will be multiplied repeatedly, such as a generator.
308
+ #
309
+ # Call this once, before the point is shared between threads. The check below is not a
310
+ # lock: the table takes long enough to build that MRI will switch threads part way
311
+ # through, so several callers can pass the check together and each build their own, each
312
+ # paying the time and the memory. What they build is identical, so whichever assignment
313
+ # lands last is still correct and no half-built table is ever visible; the cost is the
314
+ # duplicated work, and a guard that reads as protection while providing none.
315
+ #
316
+ # @param [Integer] w window width.
317
+ # @raise [PointError] Occur when this point already has precomputes.
196
318
  def calc_multiply_precomputes(w)
197
319
  raise PointError, 'This point already has precomputes.' if m_precomputes
198
320
 
@@ -208,8 +330,7 @@ module BLS
208
330
  def wNAF(n)
209
331
  w, precomputes = m_precomputes || [1, precomputes_window(1)]
210
332
  p = zero
211
- f = zero
212
- windows = (BigDecimal(max_bits) / w).ceil
333
+ windows = window_count(w)
213
334
  window_size = 2**(w - 1)
214
335
  mask = (2**w - 1)
215
336
  max_number = 2**w
@@ -222,33 +343,23 @@ module BLS
222
343
  wbits -= max_number
223
344
  n += 1
224
345
  end
225
- if wbits.zero?
226
- f += (window % 2 ? precomputes[offset].negate : precomputes[offset])
227
- else
228
- cached = precomputes[offset + wbits.abs - 1]
229
- p += (wbits.negative? ? cached.negate : cached)
230
- end
346
+ next if wbits.zero?
347
+
348
+ cached = precomputes[offset + wbits.abs - 1]
349
+ p += (wbits.negative? ? cached.negate : cached)
231
350
  end
232
- [p, f]
351
+ p
233
352
  end
234
353
  end
235
354
 
236
355
  module_function
237
356
 
238
- def norm_p1(point)
239
- point.is_a?(PointG1) ? point : PointG1.from_hex(point)
240
- end
241
-
242
- def norm_p2(point)
243
- point.is_a?(PointG2) ? point : PointG2.from_hex(point)
244
- end
245
-
246
- def norm_p1h(point)
247
- point.is_a?(PointG1) ? point : PointG1.hash_to_curve(point)
357
+ def norm_p1h(point, scheme: :basic)
358
+ point.is_a?(PointG1) ? point : PointG1.hash_to_curve(point, scheme: scheme)
248
359
  end
249
360
 
250
- def norm_p2h(point)
251
- point.is_a?(PointG2) ? point : PointG2.hash_to_curve(point)
361
+ def norm_p2h(point, scheme: :basic)
362
+ point.is_a?(PointG2) ? point : PointG2.hash_to_curve(point, scheme: scheme)
252
363
  end
253
364
 
254
365
  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.5.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.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
@@ -93,6 +73,7 @@ files:
93
73
  - bls12-381.gemspec
94
74
  - lib/bls.rb
95
75
  - lib/bls/curve.rb
76
+ - lib/bls/error.rb
96
77
  - lib/bls/field.rb
97
78
  - lib/bls/h2c.rb
98
79
  - lib/bls/math.rb
@@ -114,7 +95,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
114
95
  requirements:
115
96
  - - ">="
116
97
  - !ruby/object:Gem::Version
117
- version: 2.4.0
98
+ version: 3.3.0
118
99
  required_rubygems_version: !ruby/object:Gem::Requirement
119
100
  requirements:
120
101
  - - ">="