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 +4 -4
- data/CHANGELOG.md +57 -0
- data/README.md +101 -11
- data/bls12-381.gemspec +2 -3
- data/lib/bls/error.rb +18 -0
- data/lib/bls/field.rb +32 -2
- data/lib/bls/h2c.rb +24 -8
- data/lib/bls/math.rb +24 -4
- data/lib/bls/pairing.rb +20 -2
- data/lib/bls/point/g1.rb +66 -19
- data/lib/bls/point/g2.rb +50 -6
- data/lib/bls/point.rb +147 -36
- data/lib/bls/version.rb +1 -1
- data/lib/bls.rb +94 -24
- metadata +7 -26
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 24158dde28a5367b3fd19c5d77dfeb70ba7429c43f501f1e15345dc5792cfcd2
|
|
4
|
+
data.tar.gz: ee9dd866975f45538e1bd4cfa044fd88741cd0e01176fc6dea075111eab88cfe
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
|
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('>=
|
|
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", "
|
|
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
|
|
736
|
+
zero_0 = x0.zero? ? 1 : 0
|
|
707
737
|
sign_1 = x1 % 2
|
|
708
|
-
sign_0
|
|
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
|
-
|
|
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 = (
|
|
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 =
|
|
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
|
|
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
|
-
#
|
|
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
|
|
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 =
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
34
|
-
return 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,
|
|
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
|
-
|
|
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_,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
|
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 = (
|
|
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
|
-
|
|
189
|
-
|
|
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
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
351
|
+
p
|
|
233
352
|
end
|
|
234
353
|
end
|
|
235
354
|
|
|
236
355
|
module_function
|
|
237
356
|
|
|
238
|
-
def
|
|
239
|
-
point.is_a?(PointG1) ? point : PointG1.
|
|
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
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.
|
|
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.
|
|
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.
|
|
87
|
+
BLS.partial_pairing(g, signature)
|
|
81
88
|
else
|
|
82
|
-
BLS.
|
|
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
|
-
|
|
139
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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.
|
|
149
|
-
BLS.
|
|
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.
|
|
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:
|
|
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:
|
|
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:
|
|
98
|
+
version: 3.3.0
|
|
118
99
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
119
100
|
requirements:
|
|
120
101
|
- - ">="
|