ecdsa_ext 0.5.1 → 0.6.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: 344f77af4186a6c073efd7ceb86c1023f9395921e360a6239e6443aeaf31c957
4
- data.tar.gz: e9a32a697a607c61dd7f7993975eea458bfb09ccd1445ebe9715809b6c3620d5
3
+ metadata.gz: f8ae635cb030065845eb7e0e08d33adb64c96789e75f8e9b6eac37566e36e4d0
4
+ data.tar.gz: e4d993a63966e99d63df34b586dbc28f05e6ad093e0aa1d96134eefa93b7bad3
5
5
  SHA512:
6
- metadata.gz: b45f1ef8661a6b1a6a3b222f29b8ed5b95dbf90576040bc6cef13f295b496017508867d1ae0a6ef14c51310d349e6b01d881ba9e8f151b5fa01bc02bb6f04093
7
- data.tar.gz: e5717cb07126434a13051d760a9b2c797f6834d7e95a31a2ec433128480f55a40b2cf8e4d336913781319858e0fc7de30a00832bd3810a624a67498a9cab08ef
6
+ metadata.gz: 8ac2e8f9cc036bede6914415e81526fd82788c476ed4694e38841ea0c91176df0148b39fee7ded35c8be4fa520d0f4e9f6c53fd515397a44a14d24bc1ccef8ed
7
+ data.tar.gz: 5f3e6b6fd617492d8b8f7b8510341ffdc6fea4f16b2bac040fca0c6064d34201cba346474a78db663757d2e909a6095abb75ce3022d13da0145ca3838a302eae
data/.rubocop.yml CHANGED
@@ -2,7 +2,7 @@ require:
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
7
  TargetRubyVersion: 2.7
8
8
  RSpec/ExampleLength:
@@ -17,5 +17,6 @@ Style/WhileUntilModifier:
17
17
  Enabled: false
18
18
  Naming/MethodParameterName:
19
19
  Enabled: false
20
- RSpec/FilePath:
21
- Enabled: false
20
+ RSpec/SpecFilePathFormat:
21
+ Exclude:
22
+ - 'spec/ecdsa/ext/point_spec.rb'
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- ruby-3.3.0
1
+ ruby-4.0.0
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.6.0] - 2026-11-01
4
+
5
+ - Fix `==` of `ProjectivePoint`/`JacobianPoint` to return false for points on different groups
6
+ - Add `eql?` and `hash` to `AbstractPoint` so that points can be used as Hash keys consistently with `==`
7
+ - Fix the number of iterations in `multiply_by_scalar` to depend only on the bit length of the scalar, and document that signing is still not constant time
8
+ - Raise `ArgumentError` in `ECDSA.sign` when `temporary_key` is not in the range [1, n-1]
9
+ - Raise `ArgumentError` when constructing a point with z = 0 (use `infinity_point` instead)
10
+ - Make `ecdsa/ext/sign_verify` require `ecdsa_ext` itself so it can be required standalone
11
+ - Rewrite `sig/ecdsa_ext.rbs` to describe the actual API
12
+
13
+ ## [0.5.2] - 2026-08-07
14
+
15
+ - Fix a timing leak of the ephemeral key's bit length in `ECDSA.sign` by padding the scalar with the group order
16
+ - Check r and s against the range [1, n-1] (group order) instead of the curve's prime field in `ECDSA.check_signature!`
17
+ - Fix `NoMethodError` when comparing an infinity point with a non-infinity point via `==`
18
+
3
19
  ## [0.1.0] - 2023-02-23
4
20
 
5
21
  - Initial release
data/Gemfile CHANGED
@@ -9,9 +9,15 @@ 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"
16
16
 
17
- gem 'benchmark-ips'
17
+ gem "benchmark-ips"
18
+
19
+ # Bundled gems removed from default gems in Ruby 3.5/4.0.
20
+ gem "base64"
21
+ gem "benchmark"
22
+ gem "ostruct"
23
+ gem "racc"
data/README.md CHANGED
@@ -6,6 +6,11 @@ which mainly speeds up the computation of points on elliptic curves by using pro
6
6
  This gem was not written by a cryptography expert and has not been carefully checked as with the original gem.
7
7
  It is provided "as is" and it is the user's responsibility to make sure it will be suitable for the desired purpose.
8
8
 
9
+ In particular, this gem does **not** provide resistance against timing or other side-channel attacks.
10
+ Scalar multiplication uses a data-dependent NAF add/subtract pattern, the modular inverse is computed with
11
+ a variable-time algorithm, and Ruby's `Integer` arithmetic itself is not constant time.
12
+ Do not use this gem in an environment where an attacker can observe the timing of signing operations.
13
+
9
14
  ## Installation
10
15
 
11
16
  Add this line to your application's Gemfile:
@@ -92,3 +97,7 @@ add the following code. This code is a monkey patch to do the existing process i
92
97
  ```ruby
93
98
  require 'ecdsa/ext/sign_verify'
94
99
  ```
100
+
101
+ Note that `ECDSA.sign` provided by this patch is not constant time (see the disclaimer at the top of this README).
102
+ It only pads the ephemeral key so that the number of loop iterations in scalar multiplication is fixed,
103
+ which is a partial mitigation and not a substitute for a constant-time implementation.
@@ -20,6 +20,10 @@ module ECDSA
20
20
  raise ArgumentError, "Invalid x: #{x.inspect}" unless x.is_a?(Integer)
21
21
  raise ArgumentError, "Invalid y: #{y.inspect}" unless y.is_a?(Integer)
22
22
  raise ArgumentError, "Invalid z: #{z.inspect}" unless z.is_a?(Integer)
23
+ if z.zero?
24
+ raise ArgumentError,
25
+ "z must not be zero. Use infinity_point for the point at infinity."
26
+ end
23
27
  end
24
28
  end
25
29
 
@@ -79,7 +83,10 @@ module ECDSA
79
83
  q = self.class.infinity_point(group)
80
84
  v = self
81
85
  p, n = to_naf(x)
82
- len = [p.bit_length, n.bit_length].max
86
+ # The NAF of x has at most x.bit_length + 1 digits. Using that bound
87
+ # instead of the actual NAF length keeps the number of iterations
88
+ # dependent only on the bit length of x.
89
+ len = x.bit_length + 1
83
90
  len.times do
84
91
  q += v if p.odd?
85
92
  q += v.negate if n.odd?
@@ -107,6 +114,23 @@ module ECDSA
107
114
  raise NotImplementedError
108
115
  end
109
116
 
117
+ # Check whether same point or not.
118
+ # @param [Object] other
119
+ # @return [Boolean]
120
+ def eql?(other)
121
+ self == other
122
+ end
123
+
124
+ # Return hash value consistent with #==.
125
+ # Projective representations are not unique, so the hash is computed
126
+ # from the affine coordinates.
127
+ # @return [Integer]
128
+ def hash
129
+ return [self.class, group, :infinity].hash if infinity?
130
+ affine = to_affine
131
+ [self.class, group, affine.x, affine.y].hash
132
+ end
133
+
110
134
  private
111
135
 
112
136
  # Convert Integer to NAF
@@ -70,7 +70,9 @@ module ECDSA
70
70
  # @return [Boolean]
71
71
  def ==(other)
72
72
  return false unless other.is_a?(JacobianPoint)
73
+ return false unless other.group == group
73
74
  return true if infinity? && other.infinity?
75
+ return false if infinity? || other.infinity?
74
76
 
75
77
  zz = field.square(z)
76
78
  other_zz = field.square(other.z)
@@ -67,7 +67,9 @@ module ECDSA
67
67
 
68
68
  def ==(other)
69
69
  return false unless other.is_a?(ProjectivePoint)
70
+ return false unless other.group == group
70
71
  return true if infinity? && other.infinity?
72
+ return false if infinity? || other.infinity?
71
73
 
72
74
  lhs_x = field.mod(x * other.z)
73
75
  rhs_x = field.mod(other.x * z)
@@ -1,11 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../../ecdsa_ext"
4
+
3
5
  # A monkey patch to allow signature generation and verification of existing ECDSA with Jacobian coordinates.
4
6
  module ECDSA
5
7
  def self.sign(group, private_key, digest, temporary_key)
8
+ unless temporary_key.is_a?(Integer)
9
+ raise ArgumentError, "temporary_key is not an integer."
10
+ end
11
+ unless temporary_key.positive? && temporary_key < group.order
12
+ raise ArgumentError, "temporary_key must be in the range [1, n-1]."
13
+ end
14
+
6
15
  # Second part of step 1: Select ephemeral elliptic curve key pair
7
16
  # temporary_key was already selected for us by the caller
8
- r_point = (group.generator.to_jacobian * temporary_key).to_affine
17
+ # Pad the scalar with the group order so that its bit length, and thus
18
+ # the number of iterations in multiply_by_scalar, is fixed.
19
+ # Since (k + i * n)G = kG, this does not change the result.
20
+ # NOTE: This is only a partial mitigation of timing leaks. The NAF
21
+ # add/subtract pattern, the modular inverse of temporary_key and Ruby's
22
+ # Integer arithmetic are still data-dependent, so signing is not
23
+ # constant time.
24
+ k = temporary_key + group.order
25
+ k += group.order if k.bit_length == group.order.bit_length
26
+ r_point = (group.generator.to_jacobian * k).to_affine
9
27
 
10
28
  # Steps 2 and 3
11
29
  point_field = PrimeField.new(group.order)
@@ -29,13 +47,13 @@ module ECDSA
29
47
 
30
48
  def self.check_signature!(public_key, digest, signature)
31
49
  group = public_key.group
32
- field = group.field
50
+ point_field = PrimeField.new(group.order)
33
51
 
34
- # Step 1: r and s must be in the field and non-zero
35
- unless field.include?(signature.r)
52
+ # Step 1: r and s must be in the range [1, n-1].
53
+ unless point_field.include?(signature.r)
36
54
  raise InvalidSignatureError, "Invalid signature: r is not in the field."
37
55
  end
38
- unless field.include?(signature.s)
56
+ unless point_field.include?(signature.s)
39
57
  raise InvalidSignatureError, "Invalid signature: s is not in the field."
40
58
  end
41
59
  if signature.r.zero?
@@ -51,7 +69,6 @@ module ECDSA
51
69
  e = normalize_digest(digest, group.bit_length)
52
70
 
53
71
  # Step 4
54
- point_field = PrimeField.new(group.order)
55
72
  s_inverted = point_field.inverse(signature.s)
56
73
  u1 = point_field.mod(e * s_inverted)
57
74
  u2 = point_field.mod(signature.r * s_inverted)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ECDSA
4
4
  module Ext
5
- VERSION = "0.5.1"
5
+ VERSION = "0.6.0"
6
6
  end
7
7
  end
data/sig/ecdsa_ext.rbs CHANGED
@@ -1,4 +1,57 @@
1
- module EcdsaExt
2
- VERSION: String
3
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
1
+ # Type signatures for ecdsa_ext.
2
+ # The ecdsa gem does not ship signatures, so the classes it defines that are
3
+ # referenced here (Group, Point, PrimeField) are declared minimally.
4
+ module ECDSA
5
+ class Group
6
+ end
7
+
8
+ class PrimeField
9
+ end
10
+
11
+ class Point
12
+ def to_projective: () -> Ext::ProjectivePoint
13
+ def to_jacobian: () -> Ext::JacobianPoint
14
+ def to_hex: (?bool compression) -> String
15
+ end
16
+
17
+ module Ext
18
+ VERSION: String
19
+
20
+ class AbstractPoint
21
+ attr_reader group: Group
22
+ attr_reader x: Integer?
23
+ attr_reader y: Integer?
24
+ attr_reader z: Integer?
25
+ attr_reader infinity: bool
26
+
27
+ def initialize: (Group group, *untyped args) -> void
28
+ def self.from_affine: (Point point) -> instance
29
+ def self.infinity_point: (Group group) -> instance
30
+ def field: () -> PrimeField
31
+ def infinity_point: () -> self
32
+ def infinity?: () -> bool
33
+ def negate: () -> self
34
+ def coords: () -> [Integer?, Integer?, Integer?]
35
+ def multiply_by_scalar: (Integer x) -> self
36
+ def *: (Integer x) -> self
37
+ def add_to_point: (self other) -> self
38
+ def double: () -> self
39
+ def to_affine: () -> Point
40
+ def ==: (untyped other) -> bool
41
+ def eql?: (untyped other) -> bool
42
+ def hash: () -> Integer
43
+
44
+ private
45
+
46
+ def to_naf: (Integer x) -> [Integer, Integer]
47
+ end
48
+
49
+ class ProjectivePoint < AbstractPoint
50
+ def +: (ProjectivePoint other) -> ProjectivePoint
51
+ end
52
+
53
+ class JacobianPoint < AbstractPoint
54
+ def +: (JacobianPoint other) -> JacobianPoint
55
+ end
56
+ end
4
57
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ecdsa_ext
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.6.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-08 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"
@@ -61,7 +59,6 @@ metadata:
61
59
  homepage_uri: https://github.com/azuchi/ruby_ecdsa_ext
62
60
  source_code_uri: https://github.com/azuchi/ruby_ecdsa_ext
63
61
  changelog_uri: https://github.com/azuchi/ruby_ecdsa_ext
64
- post_install_message:
65
62
  rdoc_options: []
66
63
  require_paths:
67
64
  - lib
@@ -76,8 +73,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
76
73
  - !ruby/object:Gem::Version
77
74
  version: '0'
78
75
  requirements: []
79
- rubygems_version: 3.4.1
80
- signing_key:
76
+ rubygems_version: 4.0.3
81
77
  specification_version: 4
82
78
  summary: Extension of the ecdsa gem.
83
79
  test_files: []
data/.prettierignore DELETED
@@ -1,2 +0,0 @@
1
- /bin
2
- /vendor