h2c 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3de2455d74c152fea54a81e553295ba16f3122e1af6e3d2196e52055eb179180
4
- data.tar.gz: 6bc78d7cbfa6110f5cee57aff3d099e1a86a25f3a511b7ea13ad4b366aa38bab
3
+ metadata.gz: 9dcc054ebdb4c4b8e5526bc34cbc81c4fc19b1f20a703e410b580abcbc79bc87
4
+ data.tar.gz: 7564fca3ea320b66f3c6404a08d03dfda8e01b40a09cb56746aeabe6f9188f46
5
5
  SHA512:
6
- metadata.gz: 7654446bc65c08a1eb073f4f4de149809e9b44c58d19f9eb420ff2a6c9f9277c5e2b7e7827cde92514e3807fa1ea7544e344be9441c9ac636d256dbdff522af6
7
- data.tar.gz: 8e4f8ee2aed37e8639869ea488a276f597dc0264cacd50bd8dc1a22ff2316da3ee2adc879af92a922f54e2f6cd7307d57368ec53be8a4d95a6e551fe38465239
6
+ metadata.gz: 45e3d5f5b868bd7ca68da8831a3f3f1ef76315145374ee494ec255ec8b47098151fb4766c6b6e9180018b462cd61f80ca598b57179692c37b7d460df863862a3
7
+ data.tar.gz: 596e7f33544c8a48b337cd4867d4af5377911452a56426edc6c81616635658cca7e36b031ce1a15d44514b5532fad0000815eac1df84ecdbaaeccbe866ef4029
data/.rubocop.yml CHANGED
@@ -1,10 +1,14 @@
1
- require:
1
+ plugins:
2
2
  - rubocop-rake
3
3
  - rubocop-rspec
4
4
  inherit_gem:
5
- prettier: rubocop.yml
5
+ syntax_tree: config/rubocop.yml
6
6
  AllCops:
7
- TargetRubyVersion: 2.7
7
+ TargetRubyVersion: 3.3
8
+ NewCops: enable
9
+ # Requiring MFA on rubygems.org is a release-policy decision, not a lint one.
10
+ Gemspec/RequireMFA:
11
+ Enabled: false
8
12
  RSpec/ExampleLength:
9
13
  Enabled: false
10
14
  Metrics:
@@ -17,5 +21,7 @@ Style/WhileUntilModifier:
17
21
  Enabled: false
18
22
  Naming/MethodParameterName:
19
23
  Enabled: false
20
- RSpec/FilePath:
24
+ RSpec/SpecFilePathFormat:
25
+ Enabled: false
26
+ RSpec/SpecFilePathSuffix:
21
27
  Enabled: false
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- ruby-3.2.0
1
+ ruby-3.4.8
data/CHANGELOG.md CHANGED
@@ -1,5 +1,49 @@
1
1
  ## [Unreleased]
2
2
 
3
- ## [0.1.0] - 2022-12-26
3
+ ## [0.4.0] - 2026-11-01
4
4
 
5
- - Initial release
5
+ ### Breaking changes
6
+
7
+ - Move the curve definitions from `ECDSA::Group::Secp256k1_3ISO`, `BLS::Group::BLS12381G1` and `BLS::Group::BLS12381G1_11ISO` to `H2C::Curve::Secp256k1_3ISO`, `H2C::Curve::BLS12381G1` and `H2C::Curve::BLS12381G1_11ISO`, so that h2c no longer defines constants in the ecdsa gem's namespace or a top-level `BLS` module.
8
+ - Require Ruby 3.3 or later, matching the versions tested in CI.
9
+ - Rename `H2C::M2C::ISOGeny` to `H2C::M2C::Isogeny`.
10
+ - Remove the unused third argument (`k`) of `H2C::Expander.get`.
11
+ - `H2C::Expander::XMD#digest` now returns the `Digest` class rather than a shared instance, and `#dst` returns a binary (ASCII-8BIT) copy of the tag.
12
+
13
+ ### Fixed
14
+
15
+ - `expand_message_xmd` raised `Encoding::CompatibilityError` for a UTF-8 DST containing non-ASCII characters whose length byte is 0x80 or above.
16
+ - `expand_message_xmd` rejected valid inputs: `ell == 255`, `len_in_bytes == 65535` and DSTs of 254 or 255 bytes. The abort conditions now follow RFC 9380 Section 5.3.1 exactly.
17
+ - `H2C::M2C::SSWU#map` reduces `u` modulo p before computing `sgn0`, so an unreduced `u` maps to the same point as `u mod p`.
18
+ - `H2C::M2C::SSWU#map` raises `H2C::Error` instead of `NoMethodError` when no square root is found.
19
+
20
+ ### Changed
21
+
22
+ - `H2C::Expander::XMD` holds no mutable digest state, so an expander (and `H2C::HashToPoint`) can be shared across threads.
23
+ - Skip the cofactor multiplication when the cofactor is 1.
24
+ - Documentation now references RFC 9380 instead of draft-irtf-cfrg-hash-to-curve-16.
25
+ - Replace the unmaintained prettier gem with syntax_tree for formatting checks, since prettier 4.0.3 fails on Ruby 4.0 (`JSON.fast_generate` was removed).
26
+
27
+ ## [0.3.0] - 2026-08-09
28
+
29
+ - Fix the SSWU exceptional case (`u == 0` or `Z * u^2 == -1`) by using `inv0` semantics instead of raising.
30
+ - Fix the byte range used by `hash_to_field` when splitting the expanded message, which was wrong for extension degree `m > 1`.
31
+ - Reject an empty domain separation tag, as required by RFC 9380 Section 3.1.
32
+ - Document that the library is not constant-time and must not be used with secret inputs.
33
+ - Test against Ruby 3.3, 3.4 and 4.0.
34
+
35
+ ## [0.2.1] - 2024-02-06
36
+
37
+ - Add a `modulo` argument to `H2C::HashToPoint#hash_to_field` so that protocols such as FROST can reduce by the group order.
38
+ - Move the BLS12-381 group definitions from `ECDSA::Group` to `BLS::Group`.
39
+
40
+ ## [0.2.0] - 2023-01-13
41
+
42
+ - Support `BLS12381G1_XMD:SHA-256_SSWU_NU_` and `BLS12381G1_XMD:SHA-256_SSWU_RO_`.
43
+ - Support `P256_XMD:SHA-256_SSWU_NU_` and `P256_XMD:SHA-256_SSWU_RO_`.
44
+ - Support `P384_XMD:SHA-384_SSWU_NU_` and `P384_XMD:SHA-384_SSWU_RO_`.
45
+ - Support `P521_XMD:SHA-512_SSWU_NU_` and `P521_XMD:SHA-512_SSWU_RO_`.
46
+
47
+ ## [0.1.0] - 2023-01-12
48
+
49
+ - Initial release with `secp256k1_XMD:SHA-256_SSWU_NU_` and `secp256k1_XMD:SHA-256_SSWU_RO_`.
data/Gemfile CHANGED
@@ -9,7 +9,7 @@ gem "rake", "~> 13.0"
9
9
 
10
10
  gem "rspec", "~> 3.0"
11
11
 
12
- gem 'prettier', '4.0.3'
12
+ gem "syntax_tree"
13
13
 
14
- gem 'rubocop-rake'
15
- gem 'rubocop-rspec'
14
+ gem "rubocop-rake"
15
+ gem "rubocop-rspec"
data/README.md CHANGED
@@ -1,9 +1,23 @@
1
1
  # Hashing to Elliptic Curves for Ruby
2
2
 
3
- This is a Ruby implementation of the [Hash to Curves](https://github.com/cfrg/draft-irtf-cfrg-hash-to-curve) proposed by the [IETF](https://datatracker.ietf.org/doc/draft-irtf-cfrg-hash-to-curve/).
3
+ This is a Ruby implementation of [RFC 9380: Hashing to Elliptic Curves](https://www.rfc-editor.org/rfc/rfc9380.html).
4
4
 
5
5
  It has been tested using the Test Vector provided, but the code has not been audited to ensure functional specification and safety. Also It is under development and is subject to change without backward compatibility.
6
6
 
7
+ ## Security considerations
8
+
9
+ **This library is not constant-time and must not be used with secret input messages.**
10
+
11
+ [RFC 9380 Section 4](https://www.rfc-editor.org/rfc/rfc9380.html#section-4) requires a constant-time implementation when the message being hashed is secret. This library does not meet that requirement:
12
+
13
+ * Field arithmetic is performed with Ruby's arbitrary-precision `Integer`, whose execution time depends on the operand values.
14
+ * The mapping in `H2C::M2C::SSWU#map` selects between candidates with ordinary `if`/ternary branches rather than constant-time conditional moves.
15
+ * Byte-string handling in `H2C::Expander` allocates and copies in ways that depend on the requested length.
16
+
17
+ An attacker able to measure timing or cache behaviour may therefore be able to recover information about the input. Hashing a **public** message to a curve point (the primary use case of RFC 9380) is unaffected.
18
+
19
+ In particular, do not use this library to derive nonces or key material directly from secret values — for example, FROST's `nonce_generate`, which hashes a secret share. Such protocols require a constant-time implementation.
20
+
7
21
  The following cipher suites are currently supported:
8
22
 
9
23
  * secp256k1_XMD:SHA-256_SSWU_NU_
data/h2c.gemspec CHANGED
@@ -12,19 +12,24 @@ Gem::Specification.new do |spec|
12
12
  spec.description = "Ruby implementation of hash to curve."
13
13
  spec.homepage = "https://github.com/azuchi/h2c"
14
14
  spec.license = "MIT"
15
- spec.required_ruby_version = ">= 2.7.0"
15
+ spec.required_ruby_version = ">= 3.3.0"
16
16
 
17
17
  spec.metadata["homepage_uri"] = spec.homepage
18
- spec.metadata["source_code_uri"] = spec.homepage
19
- spec.metadata["changelog_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = spec.homepage
19
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/master/CHANGELOG.md"
20
20
 
21
21
  # Specify which files should be added to the gem when it is released.
22
22
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
- spec.files = Dir.chdir(File.expand_path(__dir__)) do
24
- `git ls-files -z`.split("\x0").reject do |f|
25
- (f == __FILE__) || f.match(%r{\A(?:(?:test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
23
+ spec.files =
24
+ Dir.chdir(File.expand_path(__dir__)) do
25
+ `git ls-files -z`.split("\x0")
26
+ .reject do |f|
27
+ (f == __FILE__) ||
28
+ f.match(
29
+ %r{\A(?:(?:test|spec|features)/|\.(?:git|travis|circleci)|appveyor)}
30
+ )
31
+ end
26
32
  end
27
- end
28
33
  spec.bindir = "exe"
29
34
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
30
35
  spec.require_paths = ["lib"]
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
- module BLS
3
- class Group
2
+ module H2C
3
+ module Curve
4
+ # BLS12-381 G1.
5
+ # https://www.rfc-editor.org/rfc/rfc9380.html#section-8.8.1
4
6
  BLS12381G1 =
5
7
  ECDSA::Group.new(
6
8
  name: "bls12381_g1",
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
- module BLS
3
- class Group
2
+ module H2C
3
+ module Curve
4
+ # 11-isogenous curve to BLS12-381 G1.
5
+ # https://www.rfc-editor.org/rfc/rfc9380.html#section-8.8.1
4
6
  BLS12381G1_11ISO =
5
7
  ECDSA::Group.new(
6
8
  name: "bls12381_g1_11iso",
@@ -1,8 +1,10 @@
1
1
  # frozen_string_literal: true
2
- module ECDSA
3
- class Group
2
+ module H2C
3
+ module Curve
4
+ # 3-isogenous curve to secp256k1.
5
+ # https://www.rfc-editor.org/rfc/rfc9380.html#section-8.7
4
6
  Secp256k1_3ISO =
5
- new(
7
+ ECDSA::Group.new(
6
8
  name: "secp256k1_3ISO",
7
9
  p:
8
10
  0xFFFFFFFF_FFFFFFFF_FFFFFFFF_FFFFFFFF_FFFFFFFF_FFFFFFFF_FFFFFFFE_FFFFFC2F,
data/lib/h2c/curve.rb ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module H2C
4
+ # Curve definitions used by the suites that are not provided by the ecdsa gem,
5
+ # including the isogenous curves required by the SSWU map for AB == 0 curves.
6
+ module Curve
7
+ end
8
+ end
9
+
10
+ require_relative "curve/secp256k1_3iso"
11
+ require_relative "curve/bls12381_g1"
12
+ require_relative "curve/bls12381_g1_11iso"
@@ -6,58 +6,55 @@ module H2C
6
6
  # Expander::XMD produces a uniformly random byte string using a cryptographic hash function H that outputs b bits.
7
7
  class XMD
8
8
  attr_reader :dst, :digest
9
+
9
10
  # Constructor
10
- # @param [String] func Hash function name. Currently supported by 'SHA256' and 'SHA512'
11
+ # @param [String] func Hash function name. Currently supported by 'SHA256', 'SHA384' and 'SHA512'
11
12
  # @param [String] dst Domain separation tag with binary format.
12
13
  def initialize(func, dst)
13
- @dst = dst
14
- @digest = Digest(func).new
14
+ # Concatenating with the length byte (and the hashed prefix for long
15
+ # tags) requires a binary string. A UTF-8 tag with non-ASCII characters
16
+ # would otherwise raise Encoding::CompatibilityError once the length
17
+ # byte is 0x80 or above.
18
+ @dst = dst.b
19
+ # Keep the Digest class rather than an instance so that expand holds no
20
+ # mutable state and a single expander can be shared across threads.
21
+ @digest = Digest(func)
15
22
  end
16
23
 
17
24
  # Expand message.
18
- # https://www.ietf.org/archive/id/draft-irtf-cfrg-hash-to-curve-16.html#name-expand_message_xmd
25
+ # https://www.rfc-editor.org/rfc/rfc9380.html#section-5.3.1
19
26
  # @param [String] msg The message to be expanded with binary format.
20
27
  # @param [Integer] len The length of the requested output in bytes.
21
28
  # @return [String] Expanded message.
22
29
  # @raise [H2C::Error]
23
30
  def expand(msg, len)
24
- b_len = digest.digest_length
31
+ b_len = digest.new.digest_length
25
32
  ell = (len + b_len - 1) / b_len
26
- dst_prime = construct_dst_prime
27
-
28
- if ell >= 0xff || len >= 0xffff || dst_prime.bytesize >= 0xff
29
- raise H2C::Error, "requested too many bytes"
33
+ # RFC 9380 5.3.1 step 2. A dst longer than 255 bytes has already been
34
+ # hashed down by construct_dst_prime, so only the output length needs
35
+ # checking here.
36
+ if ell > 255 || len > 65_535
37
+ raise H2C::Error,
38
+ "requested too many bytes: len must be at most 65535 and " \
39
+ "ceil(len / #{b_len}) at most 255."
30
40
  end
31
- lib_str = [(len >> 8) & 0xFF, (len & 0xff)].pack("CC")
32
- z_pad = Array.new(digest.block_length, 0)
33
-
34
- digest.reset
35
- digest.update(z_pad.pack("C*"))
36
- digest.update(msg)
37
- digest.update(lib_str)
38
- digest.update([0].pack("C"))
39
- digest.update(dst_prime)
40
-
41
- b0 = digest.digest
42
- digest.reset
43
- digest.update(b0)
44
- digest.update([1].pack("C"))
45
- digest.update(dst_prime)
41
+ dst_prime = construct_dst_prime
42
+ lib_str = [len].pack("n")
43
+ z_pad = "\x00".b * digest.new.block_length
46
44
 
47
- bi = digest.digest
48
- pseudo = bi
49
- (2..(ell + 1)).each do |i|
50
- digest.reset
51
- digest.update(Expander.xor(b0, bi))
52
- digest.update([i].pack("C"))
53
- digest.update(dst_prime)
54
- bi = digest.digest
55
- pseudo += bi
45
+ b0 = digest.digest(z_pad + msg.b + lib_str + "\x00".b + dst_prime)
46
+ bi = digest.digest(b0 + "\x01".b + dst_prime)
47
+ pseudo = bi.dup
48
+ # b_1 is already computed above, so this yields b_1, ..., b_ell in total.
49
+ (2..ell).each do |i|
50
+ bi = digest.digest(Expander.xor(b0, bi) + [i].pack("C") + dst_prime)
51
+ pseudo << bi
56
52
  end
57
- pseudo[0...len]
53
+ pseudo[0, len]
58
54
  end
59
55
 
60
56
  # Construct DST prime.
57
+ # https://www.rfc-editor.org/rfc/rfc9380.html#section-5.3.3
61
58
  # @return [String] DST prime
62
59
  def construct_dst_prime
63
60
  dst_prime =
data/lib/h2c/expander.rb CHANGED
@@ -31,15 +31,19 @@ module H2C
31
31
  module_function
32
32
 
33
33
  # Get expander implementation
34
- # @param [String] func Hash function name. Currently supported by 'SHA-256' and 'SHA-512'.
35
- # @raise [H2C::Error] If invalid func specified.
34
+ # @param [String] func Hash function name. One of H2C::HashFunc::XMD_FUNCS
35
+ # ("SHA256", "SHA384" or "SHA512").
36
+ # @param [String] dst Domain separation tag with binary format. Must not be empty.
37
+ # @raise [H2C::Error] If invalid func or empty dst specified.
36
38
  # @return [XMD] expander implementation, currently only XMD is supported.
37
- def get(func, dst, _k)
39
+ def get(func, dst)
38
40
  unless HashFunc::XMD_FUNCS.include?(func)
39
41
  raise H2C::Error, "func #{func} is unsupported."
40
42
  end
43
+ # RFC 9380 3.1 requires domain separation tags to have nonzero length.
44
+ raise H2C::Error, "dst must not be empty." if dst.nil? || dst.empty?
41
45
  XMD.new(func, dst)
42
- # TODO: XOR
46
+ # TODO: expand_message_xof for HashFunc::XOF_FUNCS is not implemented yet.
43
47
  end
44
48
 
45
49
  # XOR two byte(+x+ and +y+) string.
@@ -24,11 +24,22 @@ module H2C
24
24
  u = hash_to_field(msg, 1)
25
25
  suite.map.map(u[0])
26
26
  end
27
- suite.curve.cofactor ? p.multiply_by_scalar(suite.curve.cofactor) : p
27
+ clear_cofactor(p)
28
+ end
29
+
30
+ # Clear the cofactor of the point.
31
+ # https://www.rfc-editor.org/rfc/rfc9380.html#section-7
32
+ # The ecdsa gem leaves the cofactor nil for the NIST curves and set to 1 for
33
+ # secp256k1, both of which are prime-order groups with nothing to clear.
34
+ # @param [ECDSA::Point] p point
35
+ # @return [ECDSA::Point] point in the prime-order subgroup
36
+ def clear_cofactor(p)
37
+ h = suite.curve.cofactor
38
+ h && h > 1 ? p.multiply_by_scalar(h) : p
28
39
  end
29
40
 
30
41
  # Hashes a msg of any length into an element of a finite field.
31
- # https://www.ietf.org/archive/id/draft-irtf-cfrg-hash-to-curve-16.html#name-hash_to_field-implementatio
42
+ # https://www.rfc-editor.org/rfc/rfc9380.html#section-5.2
32
43
  # @param [String] msg A byte string containing the message to hash.
33
44
  # @param [Integer] count The number of elements of Field to output.
34
45
  # @param [Integer] modulo (Optional) This value is a finite field of characteristic p in the
@@ -41,8 +52,8 @@ module H2C
41
52
  count.times do |i|
42
53
  v = []
43
54
  suite.m.times do |j|
44
- offset = suite.l * (j + i * suite.m)
45
- t = pseudo[offset, (offset + suite.l)]
55
+ offset = suite.l * (j + (i * suite.m))
56
+ t = pseudo[offset, suite.l]
46
57
  vj = t.unpack1("H*").to_i(16)
47
58
  v[j] = vj % modulo
48
59
  end
@@ -2,9 +2,9 @@
2
2
 
3
3
  module H2C
4
4
  module M2C
5
- module ISOGeny
5
+ module Isogeny
6
6
  # 11-isogeny map for BLS12381
7
- # https://www.ietf.org/archive/id/draft-irtf-cfrg-hash-to-curve-16.html#appendix-E.2
7
+ # https://www.rfc-editor.org/rfc/rfc9380.html#appendix-E.2
8
8
  class BLS12381G1
9
9
  attr_reader :e0, :e1
10
10
 
@@ -76,8 +76,8 @@ module H2C
76
76
  1
77
77
  ].freeze
78
78
  def initialize
79
- @e0 = BLS::Group::BLS12381G1_11ISO
80
- @e1 = BLS::Group::BLS12381G1
79
+ @e0 = Curve::BLS12381G1_11ISO
80
+ @e1 = Curve::BLS12381G1
81
81
  end
82
82
 
83
83
  def map(x, y)
@@ -87,12 +87,12 @@ module H2C
87
87
  y_num = 0
88
88
  y_den = 0
89
89
  (X_NUM.length - 1).step(0, -1) do |i|
90
- x_num = f.mod(x_num * x + X_NUM[i])
91
- x_den = f.mod(x_den * x + X_DEN[i])
90
+ x_num = f.mod((x_num * x) + X_NUM[i])
91
+ x_den = f.mod((x_den * x) + X_DEN[i])
92
92
  end
93
93
  (Y_NUM.length - 1).step(0, -1) do |i|
94
- y_num = f.mod(y_num * x + Y_NUM[i])
95
- y_den = f.mod(y_den * x + Y_DEN[i])
94
+ y_num = f.mod((y_num * x) + Y_NUM[i])
95
+ y_den = f.mod((y_den * x) + Y_DEN[i])
96
96
  end
97
97
  xx = f.mod(x_num * f.inverse(x_den))
98
98
  yy = f.mod(y * (y_num * f.inverse(y_den)))
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
  module H2C
3
3
  module M2C
4
- module ISOGeny
4
+ module Isogeny
5
5
  # 3-isogeny map for secp256k1
6
- # https://www.ietf.org/archive/id/draft-irtf-cfrg-hash-to-curve-16.html#appendix-E.1
6
+ # https://www.rfc-editor.org/rfc/rfc9380.html#appendix-E.1
7
7
  class Secp256k1
8
8
  attr_reader :e0, :e1
9
9
 
@@ -33,7 +33,7 @@ module H2C
33
33
  ].freeze
34
34
 
35
35
  def initialize
36
- @e0 = ECDSA::Group::Secp256k1_3ISO
36
+ @e0 = Curve::Secp256k1_3ISO
37
37
  @e1 = ECDSA::Group::Secp256k1
38
38
  end
39
39
 
@@ -45,10 +45,10 @@ module H2C
45
45
  y_num = 0
46
46
  y_den = 0
47
47
  3.step(0, -1) do |i|
48
- x_num = f.mod(x_num * x + X_NUM[i])
49
- x_den = f.mod(x_den * x + X_DEN[i])
50
- y_num = f.mod(y_num * x + Y_NUM[i])
51
- y_den = f.mod(y_den * x + Y_DEN[i])
48
+ x_num = f.mod((x_num * x) + X_NUM[i])
49
+ x_den = f.mod((x_den * x) + X_DEN[i])
50
+ y_num = f.mod((y_num * x) + Y_NUM[i])
51
+ y_den = f.mod((y_den * x) + Y_DEN[i])
52
52
  end
53
53
  xx = f.mod(x_num * f.inverse(x_den))
54
54
  yy = f.mod(y * (y_num * f.inverse(y_den)))
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
  module H2C
3
3
  module M2C
4
- # https://www.ietf.org/archive/id/draft-irtf-cfrg-hash-to-curve-16.html#name-isogeny-maps-for-suites
5
- module ISOGeny
4
+ # https://www.rfc-editor.org/rfc/rfc9380.html#appendix-E
5
+ module Isogeny
6
6
  autoload :Secp256k1, "h2c/m2c/isogeny/secp256k1"
7
7
  autoload :BLS12381G1, "h2c/m2c/isogeny/bls12381_g1"
8
8
  end
data/lib/h2c/m2c/sswu.rb CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  module H2C
4
4
  module M2C
5
- # https://www.ietf.org/archive/id/draft-irtf-cfrg-hash-to-curve-16.html#section-6.6.2
5
+ # https://www.rfc-editor.org/rfc/rfc9380.html#section-6.6.2
6
6
  class SSWU
7
7
  attr_reader :curve, :c1, :c2, :z
8
8
 
@@ -14,21 +14,26 @@ module H2C
14
14
  @z = z
15
15
  f = curve.field
16
16
  @c1 = f.mod(-curve.param_b * f.inverse(curve.param_a))
17
- @c2 = f.mod(-f.inverse(z))
17
+ @c2 = f.mod(-f.inverse(f.mod(z)))
18
18
  end
19
19
 
20
20
  # Outputs x and y are elements of the field F.
21
21
  # @param [Integer] u
22
- # @return [Array(Integer, Integer)] x and y
22
+ # @return [ECDSA::Point] point on the curve
23
+ # @raise [H2C::Error] if no square root is found, which cannot happen for
24
+ # a correct Z but guards against a misconfigured curve.
23
25
  def map(u)
24
26
  f = curve.field
27
+ # sgn0 below inspects the parity of u, so u must be reduced first.
28
+ u = f.mod(u)
25
29
  t1 = f.mod(f.power(u, 2) * f.mod(z))
26
30
  t2 = f.power(t1, 2)
27
31
  x1 = f.mod(t1 + t2)
28
- x1 = f.inverse(x1)
29
32
  e1 = x1.zero?
33
+ # inv0(x) is the multiplicative inverse of x, with inv0(0) = 0.
34
+ x1 = e1 ? 0 : f.inverse(x1)
30
35
  x1 = f.mod(x1 + 1)
31
- x1 = e1 ? c2 : x1
36
+ x1 = c2 if e1
32
37
  x1 = f.mod(x1 * c1)
33
38
  gx1 = f.power(x1, 2)
34
39
  gx1 = f.mod(gx1 + curve.param_a)
@@ -40,20 +45,23 @@ module H2C
40
45
  e2 = square?(gx1)
41
46
  x = e2 ? x1 : x2
42
47
  y2 = e2 ? gx1 : gx2
43
- y = f.square_roots(y2)[0]
48
+ y = f.square_roots(y2).first
49
+ if y.nil?
50
+ raise H2C::Error, "no square root found for #{y2} on #{curve.name}."
51
+ end
44
52
  e3 = sgn0(u) == sgn0(y)
45
53
  y = f.mod(e3 ? y : -y)
46
54
  curve.new_point([x, y])
47
55
  end
48
56
 
49
57
  def square?(x)
50
- test = curve.field.power(x, ((curve.field.prime - 1) / 2))
58
+ test = curve.field.power(x, (curve.field.prime - 1) / 2)
51
59
  [0, 1].include?(test)
52
60
  end
53
61
 
54
62
  def sgn0(x)
55
63
  res = x % 2
56
- curve.field.mod(1 - 2 * res)
64
+ curve.field.mod(1 - (2 * res))
57
65
  end
58
66
  end
59
67
  end
@@ -1,19 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
  module H2C
3
3
  module M2C
4
- # https://www.ietf.org/archive/id/draft-irtf-cfrg-hash-to-curve-16.html#section-6.6.3
4
+ # https://www.rfc-editor.org/rfc/rfc9380.html#section-6.6.3
5
5
  class SSWUAB0
6
6
  attr_reader :sswu, :iso
7
7
 
8
8
  # Constructor
9
- # @param [H2C::M2C::ISOGeny] iso
9
+ # @param [H2C::M2C::Isogeny] iso
10
10
  # @param [Integer] z
11
11
  def initialize(iso, z)
12
12
  @sswu = SSWU.new(iso.e0, z)
13
13
  @iso = iso
14
14
  end
15
15
 
16
- # https://www.ietf.org/archive/id/draft-irtf-cfrg-hash-to-curve-16.html#section-6.6.3
16
+ # https://www.rfc-editor.org/rfc/rfc9380.html#section-6.6.3
17
17
  # @param [Integer] u
18
18
  # @return [ECDSA::Point]
19
19
  def map(u)
data/lib/h2c/m2c.rb CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  module H2C
4
4
  # Map to Curve functions.
5
- # https://www.ietf.org/archive/id/draft-irtf-cfrg-hash-to-curve-16.html#section-6
5
+ # https://www.rfc-editor.org/rfc/rfc9380.html#section-6
6
6
  module M2C
7
- autoload :ISOGeny, "h2c/m2c/isogeny"
7
+ autoload :Isogeny, "h2c/m2c/isogeny"
8
8
  autoload :SSWU, "h2c/m2c/sswu"
9
9
  autoload :SSWUAB0, "h2c/m2c/sswuab0"
10
10
  end
data/lib/h2c/suite.rb CHANGED
@@ -25,38 +25,38 @@ module H2C
25
25
  case id
26
26
  when SECP256K1_XMDSHA256_SSWU_NU_, SECP256K1_XMDSHA256_SSWU_RO_
27
27
  @curve = ECDSA::Group::Secp256k1
28
- @exp = Expander.get(HashFunc::SHA256, dst, @k)
28
+ @exp = Expander.get(HashFunc::SHA256, dst)
29
29
  @l = 48
30
- @map = M2C::SSWUAB0.new(H2C::M2C::ISOGeny::Secp256k1.new, -11)
30
+ @map = M2C::SSWUAB0.new(H2C::M2C::Isogeny::Secp256k1.new, -11)
31
31
  @ro = (id == SECP256K1_XMDSHA256_SSWU_RO_)
32
32
  when BLS12381G1_XMDSHA256_SSWU_NU_, BLS12381G1_XMDSHA256_SSWU_RO_
33
- @curve = BLS::Group::BLS12381G1
34
- @exp = Expander.get(HashFunc::SHA256, dst, @k)
33
+ @curve = Curve::BLS12381G1
34
+ @exp = Expander.get(HashFunc::SHA256, dst)
35
35
  @l = 64
36
- @map = M2C::SSWUAB0.new(H2C::M2C::ISOGeny::BLS12381G1.new, 11)
36
+ @map = M2C::SSWUAB0.new(H2C::M2C::Isogeny::BLS12381G1.new, 11)
37
37
  @ro = (id == BLS12381G1_XMDSHA256_SSWU_RO_)
38
38
  when P256_XMDSHA256_SSWU_NU_, P256_XMDSHA256_SSWU_RO_
39
39
  @curve = ECDSA::Group::Nistp256
40
- @exp = Expander.get(HashFunc::SHA256, dst, @k)
40
+ @exp = Expander.get(HashFunc::SHA256, dst)
41
41
  @l = 48
42
42
  @map = M2C::SSWU.new(ECDSA::Group::Nistp256, -10)
43
43
  @ro = (id == P256_XMDSHA256_SSWU_RO_)
44
44
  when P384_XMDSHA384_SSWU_NU_, P384_XMDSHA384_SSWU_RO_
45
45
  @k = 192
46
46
  @curve = ECDSA::Group::Nistp384
47
- @exp = Expander.get(HashFunc::SHA384, dst, @k)
47
+ @exp = Expander.get(HashFunc::SHA384, dst)
48
48
  @l = 72
49
49
  @map = M2C::SSWU.new(ECDSA::Group::Nistp384, -12)
50
50
  @ro = (id == P384_XMDSHA384_SSWU_RO_)
51
51
  when P521_XMDSHA512_SSWU_NU_, P521_XMDSHA512_SSWU_RO_
52
52
  @k = 256
53
53
  @curve = ECDSA::Group::Nistp521
54
- @exp = Expander.get(HashFunc::SHA512, dst, @k)
54
+ @exp = Expander.get(HashFunc::SHA512, dst)
55
55
  @l = 98
56
56
  @map = M2C::SSWU.new(ECDSA::Group::Nistp521, -4)
57
57
  @ro = (id == P521_XMDSHA512_SSWU_RO_)
58
58
  else
59
- raise H2C::Error, "suite #{curve} unsupported."
59
+ raise H2C::Error, "suite #{id} unsupported."
60
60
  end
61
61
  end
62
62
  end
data/lib/h2c/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module H2C
4
- VERSION = "0.2.1"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/h2c.rb CHANGED
@@ -2,10 +2,10 @@
2
2
 
3
3
  require_relative "h2c/version"
4
4
  require "ecdsa"
5
- require_relative "ext/curve"
5
+ require_relative "h2c/curve"
6
6
 
7
7
  # Hash to Curves library.
8
- # https://www.ietf.org/archive/id/draft-irtf-cfrg-hash-to-curve-16.html
8
+ # https://www.rfc-editor.org/rfc/rfc9380.html
9
9
  module H2C
10
10
  class Error < StandardError
11
11
  end
@@ -31,9 +31,10 @@ module H2C
31
31
 
32
32
  # Get hash to curve corresponding to +suite+.
33
33
  #
34
- # @param [String] suite Suite name for hashing.
35
- # Currently supported are "secp256k1_XMD:SHA-256_SSWU_NU_" and "secp256k1_XMD:SHA-256_SSWU_RO_".
36
- # @param [String] dst Domain separation tag.
34
+ # @param [String] suite Suite id for hashing. See the constants in H2C::Suite
35
+ # for the supported suites.
36
+ # @param [String] dst Domain separation tag. Must not be empty.
37
+ # @raise [H2C::Error] If the suite is unsupported or dst is empty.
37
38
  # @return [H2C::HashToPoint]
38
39
  def get(suite, dst)
39
40
  suite = Suite.new(suite, dst)
data/sig/h2c.rbs CHANGED
@@ -1,4 +1,127 @@
1
+ # The ecdsa gem ships no type signatures, so its classes (ECDSA::Group,
2
+ # ECDSA::Point and ECDSA::PrimeField) appear here as untyped.
1
3
  module H2C
2
4
  VERSION: String
3
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
+
6
+ class Error < StandardError
7
+ end
8
+
9
+ module HashFunc
10
+ SHA256: String
11
+ SHA384: String
12
+ SHA512: String
13
+ SHAKE128: String
14
+ SHAKE256: String
15
+ XMD_FUNCS: Array[String]
16
+ XOF_FUNCS: Array[String]
17
+ end
18
+
19
+ # Returns a hash-to-curve function for +suite+ and +dst+.
20
+ def self.get: (String suite, String dst) -> HashToPoint
21
+
22
+ module Curve
23
+ Secp256k1_3ISO: untyped
24
+ BLS12381G1: untyped
25
+ BLS12381G1_11ISO: untyped
26
+ end
27
+
28
+ class Suite
29
+ SECP256K1_XMDSHA256_SSWU_NU_: String
30
+ SECP256K1_XMDSHA256_SSWU_RO_: String
31
+ BLS12381G1_XMDSHA256_SSWU_NU_: String
32
+ BLS12381G1_XMDSHA256_SSWU_RO_: String
33
+ P256_XMDSHA256_SSWU_NU_: String
34
+ P256_XMDSHA256_SSWU_RO_: String
35
+ P384_XMDSHA384_SSWU_NU_: String
36
+ P384_XMDSHA384_SSWU_RO_: String
37
+ P521_XMDSHA512_SSWU_NU_: String
38
+ P521_XMDSHA512_SSWU_RO_: String
39
+
40
+ attr_reader id: String
41
+ attr_reader curve: untyped
42
+ attr_reader k: Integer
43
+ attr_reader exp: Expander::XMD
44
+ attr_reader m: Integer
45
+ attr_reader l: Integer
46
+ attr_reader ro: bool
47
+ attr_reader map: M2C::SSWU | M2C::SSWUAB0
48
+
49
+ def initialize: (String id, String dst) -> void
50
+ end
51
+
52
+ class HashToPoint
53
+ attr_reader suite: Suite
54
+
55
+ def initialize: (Suite suite) -> void
56
+ def digest: (String msg) -> untyped
57
+ def clear_cofactor: (untyped p) -> untyped
58
+ def hash_to_field: (String msg, Integer count, ?Integer modulo) -> Array[Integer]
59
+ end
60
+
61
+ module Expander
62
+ MAX_DST_LENGTH: Integer
63
+ LONG_DST_PREFIX: String
64
+
65
+ def self.get: (String func, String? dst) -> XMD
66
+ def self.xor: (String x, String y) -> String
67
+
68
+ class XMD
69
+ attr_reader dst: String
70
+ attr_reader digest: singleton(Digest::Base)
71
+
72
+ def initialize: (String func, String dst) -> void
73
+ def expand: (String msg, Integer len) -> String
74
+ def construct_dst_prime: () -> String
75
+ end
76
+ end
77
+
78
+ module M2C
79
+ class SSWU
80
+ attr_reader curve: untyped
81
+ attr_reader c1: Integer
82
+ attr_reader c2: Integer
83
+ attr_reader z: Integer
84
+
85
+ def initialize: (untyped curve, Integer z) -> void
86
+ def map: (Integer u) -> untyped
87
+ def square?: (Integer x) -> bool
88
+ def sgn0: (Integer x) -> Integer
89
+ end
90
+
91
+ class SSWUAB0
92
+ attr_reader sswu: SSWU
93
+ attr_reader iso: Isogeny::Secp256k1 | Isogeny::BLS12381G1
94
+
95
+ def initialize: (Isogeny::Secp256k1 | Isogeny::BLS12381G1 iso, Integer z) -> void
96
+ def map: (Integer u) -> untyped
97
+ end
98
+
99
+ module Isogeny
100
+ class Secp256k1
101
+ X_NUM: Array[Integer]
102
+ X_DEN: Array[Integer]
103
+ Y_NUM: Array[Integer]
104
+ Y_DEN: Array[Integer]
105
+
106
+ attr_reader e0: untyped
107
+ attr_reader e1: untyped
108
+
109
+ def initialize: () -> void
110
+ def map: (Integer x, Integer y) -> [Integer, Integer]
111
+ end
112
+
113
+ class BLS12381G1
114
+ X_NUM: Array[Integer]
115
+ X_DEN: Array[Integer]
116
+ Y_NUM: Array[Integer]
117
+ Y_DEN: Array[Integer]
118
+
119
+ attr_reader e0: untyped
120
+ attr_reader e1: untyped
121
+
122
+ def initialize: () -> void
123
+ def map: (Integer x, Integer y) -> [Integer, Integer]
124
+ end
125
+ end
126
+ end
4
127
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: h2c
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - azuchi
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2024-02-06 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: ecdsa
@@ -31,7 +30,6 @@ executables: []
31
30
  extensions: []
32
31
  extra_rdoc_files: []
33
32
  files:
34
- - ".prettierignore"
35
33
  - ".rspec"
36
34
  - ".rubocop.yml"
37
35
  - ".ruby-gemset"
@@ -45,11 +43,11 @@ files:
45
43
  - bin/console
46
44
  - bin/setup
47
45
  - h2c.gemspec
48
- - lib/ext/curve.rb
49
- - lib/ext/curve/bls12381_g1.rb
50
- - lib/ext/curve/bls12381_g1_11iso.rb
51
- - lib/ext/curve/secp256k1_3iso.rb
52
46
  - lib/h2c.rb
47
+ - lib/h2c/curve.rb
48
+ - lib/h2c/curve/bls12381_g1.rb
49
+ - lib/h2c/curve/bls12381_g1_11iso.rb
50
+ - lib/h2c/curve/secp256k1_3iso.rb
53
51
  - lib/h2c/expander.rb
54
52
  - lib/h2c/expander/xmd.rb
55
53
  - lib/h2c/hash_to_point.rb
@@ -68,8 +66,7 @@ licenses:
68
66
  metadata:
69
67
  homepage_uri: https://github.com/azuchi/h2c
70
68
  source_code_uri: https://github.com/azuchi/h2c
71
- changelog_uri: https://github.com/azuchi/h2c
72
- post_install_message:
69
+ changelog_uri: https://github.com/azuchi/h2c/blob/master/CHANGELOG.md
73
70
  rdoc_options: []
74
71
  require_paths:
75
72
  - lib
@@ -77,15 +74,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
77
74
  requirements:
78
75
  - - ">="
79
76
  - !ruby/object:Gem::Version
80
- version: 2.7.0
77
+ version: 3.3.0
81
78
  required_rubygems_version: !ruby/object:Gem::Requirement
82
79
  requirements:
83
80
  - - ">="
84
81
  - !ruby/object:Gem::Version
85
82
  version: '0'
86
83
  requirements: []
87
- rubygems_version: 3.4.1
88
- signing_key:
84
+ rubygems_version: 3.6.9
89
85
  specification_version: 4
90
86
  summary: Ruby implementation of hash to curve.
91
87
  test_files: []
data/.prettierignore DELETED
@@ -1,2 +0,0 @@
1
- /bin
2
- /vendor
data/lib/ext/curve.rb DELETED
@@ -1,5 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "curve/secp256k1_3iso"
4
- require_relative "curve/bls12381_g1"
5
- require_relative "curve/bls12381_g1_11iso"