derivator 0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6fb64625fb015d83f804ddd0323ebc07825ea569dd1a1f2c739d7e8e143b7e65
4
+ data.tar.gz: 5f3519ad198b4ca049be879525df4e0d987a37f4a56b4969871188b007b132c3
5
+ SHA512:
6
+ metadata.gz: e3e67c14744d3c3173178f17c6a6003694abc095b3a10a78a40c42f16f78fd1e8d25e937df05ebd793b4d4d6bd01013a5e923ad3508e23e0eae5106e7e25507e
7
+ data.tar.gz: 1abba063b0c1dfbbeb38e7f0db23d87da0179d021f25e0003dee1df0ffd9255504fbdfef34245eb4b9809d83cd8c9591b6f71ea0030a2ae38d158a22baeef011
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in derivator.gemspec
4
+ gemspec
5
+
6
+ gem 'rake', '~> 13.0'
7
+
8
+ gem 'rspec', '~> 3.0'
data/Gemfile.lock ADDED
@@ -0,0 +1,41 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ derivator (0.1)
5
+ openssl (>= 3.0)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ diff-lcs (1.5.0)
11
+ openssl (3.0.0)
12
+ rake (13.0.6)
13
+ rspec (3.11.0)
14
+ rspec-core (~> 3.11.0)
15
+ rspec-expectations (~> 3.11.0)
16
+ rspec-mocks (~> 3.11.0)
17
+ rspec-core (3.11.0)
18
+ rspec-support (~> 3.11.0)
19
+ rspec-expectations (3.11.0)
20
+ diff-lcs (>= 1.2.0, < 2.0)
21
+ rspec-support (~> 3.11.0)
22
+ rspec-mocks (3.11.1)
23
+ diff-lcs (>= 1.2.0, < 2.0)
24
+ rspec-support (~> 3.11.0)
25
+ rspec-support (3.11.0)
26
+ webrick (1.7.0)
27
+ yard (0.9.28)
28
+ webrick (~> 1.7.0)
29
+
30
+ PLATFORMS
31
+ ruby
32
+ x86_64-linux
33
+
34
+ DEPENDENCIES
35
+ derivator!
36
+ rake (~> 13.0)
37
+ rspec (~> 3.0)
38
+ yard
39
+
40
+ BUNDLED WITH
41
+ 2.2.22
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2022 WAGMI LTD.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,115 @@
1
+ # Derivator
2
+
3
+ Ruby implementation of EC HD key derivation ([SLIP10](https://github.com/satoshilabs/slips/blob/master/slip-0010.md), [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki)) and mnemonic sentence interpretation ([BIP39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki)).
4
+
5
+ Supports secp256k1 (Bitcoin), nist256p1 (P-256) and ed25519 (Edwards 25519) elliptic curves.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'derivator'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle install
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install derivator
22
+
23
+ ## Usage
24
+
25
+ ### Generate mnemonic
26
+
27
+ ```bash
28
+ # bash
29
+ derivator_mnemonic
30
+ ```
31
+
32
+ ```ruby
33
+ # ruby
34
+ require 'derivator'
35
+
36
+ puts Derivator::Mnemonic.generate
37
+ ```
38
+
39
+ ### Generate seed from mnemonic
40
+
41
+ ```bash
42
+ # bash
43
+ echo "finish merry file canoe cruel meadow spoil sunset pigeon depend brush step" | \
44
+ derivator_seed_from_mnemonic
45
+
46
+ echo "spike kit woman maze culture uncle way tobacco saddle silly sunset certain" | \
47
+ derivator_seed_from_mnemonic "my_mnemonic_password"
48
+ ```
49
+
50
+ ```ruby
51
+ # ruby
52
+ require 'derivator'
53
+
54
+ mnemonic = 'finish merry file canoe cruel meadow spoil sunset pigeon depend brush step'
55
+ puts Derivator::Mnemonic.seed(mnemonic)
56
+
57
+ mnemonic = 'spike kit woman maze culture uncle way tobacco saddle silly sunset certain'
58
+ puts Derivator::Mnemonic.seed(mnemonic, 'my_mnemonic_password')
59
+ ```
60
+
61
+ ### Generate master key and chain code from seed
62
+
63
+ ```bash
64
+ # bash
65
+ echo 000102030405060708090a0b0c0d0e0f | derivator_key_from_seed ed25519
66
+ ```
67
+
68
+ ```ruby
69
+ # ruby
70
+ require 'derivator'
71
+
72
+ seed = '000102030405060708090a0b0c0d0e0f'
73
+ key = Derivator::Key.from_seed(seed, :ed25519)
74
+ puts "#{key.private_key_hex} #{key.chain_code_hex}"
75
+ ```
76
+
77
+ ### Derive child key from master key and chain code
78
+
79
+ ```bash
80
+ # bash
81
+ echo 2b4be7f19ee27bbf30c667b642d5f4aa69fd169872f8fc3059c08ebae2eb19e7 90046a93de5380a72b5e45010748567d5ea02bbf6522f979e05c0d8d8ca9fffb \
82
+ derivator_key_from_parent "m/0'/1'/2'/2'/1000000000'" ed25519
83
+ ```
84
+
85
+ ```ruby
86
+ # ruby
87
+ require 'derivator'
88
+
89
+ master_private_key_hex = '2b4be7f19ee27bbf30c667b642d5f4aa69fd169872f8fc3059c08ebae2eb19e7'
90
+ master_chain_code_hex = '90046a93de5380a72b5e45010748567d5ea02bbf6522f979e05c0d8d8ca9fffb'
91
+ key = Derivator::Key.from_hex(master_private_key_hex, master_chain_code_hex, :ed25519)
92
+ derived_key = key.derive("m/0'/1'/2'/2'/1000000000'")
93
+ puts "#{derived_key.private_key_hex} #{derived_key.chain_code_hex}"
94
+ ```
95
+
96
+ ### Derive public key from private key
97
+
98
+ ```bash
99
+ # bash
100
+ echo 2b4be7f19ee27bbf30c667b642d5f4aa69fd169872f8fc3059c08ebae2eb19e7 | \
101
+ derivator_public_from_private ed25519
102
+ ```
103
+
104
+ ```ruby
105
+ # ruby
106
+ require 'derivator'
107
+
108
+ private_key_hex = '2b4be7f19ee27bbf30c667b642d5f4aa69fd169872f8fc3059c08ebae2eb19e7'
109
+ key = Derivator::Key.from_hex(private_key_hex, '', :ed25519)
110
+ puts key.public_key_hex
111
+ ```
112
+
113
+ ## License
114
+
115
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
data/bin/console ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'derivator'
5
+ require 'derivator/refinements'
6
+
7
+ include Derivator
8
+
9
+ using Refinements
10
+
11
+ require 'irb'
12
+ IRB.start(__FILE__)
data/derivator.gemspec ADDED
@@ -0,0 +1,27 @@
1
+ Gem::Specification.new do |spec|
2
+ spec.name = 'derivator'
3
+ spec.version = '0.1'
4
+ spec.authors = ['WAGMI LTD.']
5
+ spec.email = ['debifi@debifi.com']
6
+
7
+ spec.summary = 'BIP-0039, BIP-0032, SLIP-0010 elliptic curve (P-256, ED25519, SECP256K1) keys derivator'
8
+ spec.homepage = 'https://gitlab.com/debifi-public/derivator'
9
+ spec.license = 'MIT'
10
+ spec.required_ruby_version = '>= 2.4.0'
11
+
12
+ spec.metadata['homepage_uri'] = spec.homepage
13
+ spec.metadata['source_code_uri'] = spec.homepage
14
+
15
+ # Specify which files should be added to the gem when it is released.
16
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
17
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
18
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
19
+ end
20
+ spec.bindir = 'exe'
21
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
22
+ spec.require_paths = ['lib']
23
+
24
+ spec.add_development_dependency 'yard'
25
+
26
+ spec.add_dependency 'openssl', '>= 3.0'
27
+ end
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ git_path = File.expand_path("../.git", __dir__)
4
+
5
+ if File.exist?(git_path)
6
+ lib_path = File.expand_path("../lib", __dir__)
7
+ $:.unshift(lib_path)
8
+ end
9
+
10
+ require 'derivator'
11
+
12
+ path = ARGV.first
13
+ curve = ARGV[1] || 'secp256k1'
14
+ private_key_hex, chain_code_hex = STDIN.gets.split
15
+ key = Derivator::Key.from_hex(private_key_hex, chain_code_hex, curve.to_sym)
16
+ derived_key = key.derive(path)
17
+ puts "#{derived_key.private_key_hex} #{derived_key.chain_code_hex}"
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ git_path = File.expand_path("../.git", __dir__)
4
+
5
+ if File.exist?(git_path)
6
+ lib_path = File.expand_path("../lib", __dir__)
7
+ $:.unshift(lib_path)
8
+ end
9
+
10
+ require 'derivator'
11
+
12
+ curve = ARGV.first || 'secp256k1'
13
+ seed_hex = STDIN.gets.chomp
14
+ key = Derivator::Key.from_seed(seed_hex, curve.to_sym)
15
+ puts "#{key.private_key_hex} #{key.chain_code_hex}"
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ git_path = File.expand_path("../.git", __dir__)
4
+
5
+ if File.exist?(git_path)
6
+ lib_path = File.expand_path("../lib", __dir__)
7
+ $:.unshift(lib_path)
8
+ end
9
+
10
+ require 'derivator'
11
+
12
+ puts Derivator::Mnemonic.generate
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ git_path = File.expand_path("../.git", __dir__)
4
+
5
+ if File.exist?(git_path)
6
+ lib_path = File.expand_path("../lib", __dir__)
7
+ $:.unshift(lib_path)
8
+ end
9
+
10
+ require 'derivator'
11
+
12
+ private_key = STDIN.gets.split.first
13
+ curve = ARGV.first || 'secp256k1'
14
+ key = Derivator::Key.from_hex(private_key, '', curve.to_sym)
15
+ puts key.public_key_hex
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ git_path = File.expand_path("../.git", __dir__)
4
+
5
+ if File.exist?(git_path)
6
+ lib_path = File.expand_path("../lib", __dir__)
7
+ $:.unshift(lib_path)
8
+ end
9
+
10
+ require 'derivator'
11
+
12
+ mnemonic = STDIN.gets
13
+ password = ARGV.first
14
+ puts Derivator::Mnemonic.seed(mnemonic, password)
@@ -0,0 +1,300 @@
1
+ require_relative 'refinements'
2
+
3
+ module Derivator
4
+ # {https://github.com/satoshilabs/slips/blob/master/slip-0010.md SLIP10} key derivation.
5
+ class Key
6
+ # secp256k1 EC private key binary prefix when DER-encoded
7
+ SECP256K1_DER_PRIVATE_PREFIX = '303e020100301006072a8648ce3d020106052b8104000a042730250201010420'
8
+ # nist256p1 EC private key binary prefix when DER-encoded
9
+ NIST256P1_DER_PRIVATE_PREFIX = '3041020100301306072a8648ce3d020106082a8648ce3d030107042730250201010420'
10
+ # ed25519 EC private key binary prefix when DER-encoded
11
+ ED25519_DER_PRIVATE_PREFIX = '302e020100300506032b657004220420'
12
+ # ed25519 EC public key binary prefix when DER-encoded
13
+ ED25519_DER_PUBLIC_PREFIX = '302a300506032b65700321'
14
+
15
+ # secp256k1 BIP32/SLIP10 seed key
16
+ SECP256K1_SEED_KEY = 'Bitcoin seed'
17
+ # nist256p1 SLIP10 seed key
18
+ NIST256P1_SEED_KEY = 'Nist256p1 seed'
19
+ # ed25519 SLIP10 seed key
20
+ ED25519_SEED_KEY = 'ed25519 seed'
21
+
22
+ # secp256k1 largest valid private key
23
+ SECP256K1_LARGEST_KEY = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140
24
+ # nist256p1 largest valid private key
25
+ NIST256P1_LARGEST_KEY = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551
26
+ # ed25519 largest valid private key (unlimited)
27
+ ED25519_LARGEST_KEY = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
28
+
29
+ # @return [Symbol] EC curve used for the key (<code>:secp256k1</code>, <code>:nist256p1</code> or <code>:ed25519</code>).
30
+ attr_reader :curve
31
+
32
+ # @return [String] private key (in binary format).
33
+ attr_reader :private_key
34
+
35
+ # @return [String] chain code (in binary format).
36
+ attr_reader :chain_code
37
+
38
+ using Refinements
39
+
40
+ # Creates new key from private key and chain code hex strings.
41
+ #
42
+ # @param private_key_hex [String] private key hex string
43
+ # @param chain_code_hex [String] chain code hex string
44
+ # @param curve [Symbol] curve (<code>:secp256k1</code>, <code>:nist256p1</code> or <code>:ed25519</code>)
45
+ # @return [Key] new key
46
+ def self.from_hex(private_key_hex, chain_code_hex, curve = :secp256k1)
47
+ new(private_key_hex.from_hex, chain_code_hex.from_hex, curve)
48
+ end
49
+
50
+ # Creates new key from seed hex string.
51
+ #
52
+ # @param seed_hex [String] seed hex string
53
+ # @param curve [Symbol] curve (<code>:secp256k1</code>, <code>:nist256p1</code> or <code>:ed25519</code>)
54
+ # @return [Key] new key
55
+ def self.from_seed(seed_hex, curve = :secp256k1)
56
+ seed_bytes = seed_hex.from_hex
57
+ seed_key =
58
+ case curve
59
+ when :secp256k1
60
+ SECP256K1_SEED_KEY
61
+ when :nist256p1
62
+ NIST256P1_SEED_KEY
63
+ when :ed25519
64
+ ED25519_SEED_KEY
65
+ end
66
+
67
+ hmac =
68
+ OpenSSL::HMAC.hexdigest(
69
+ "SHA512",
70
+ seed_key,
71
+ seed_bytes
72
+ )
73
+
74
+ private_key_hex = hmac[0..63]
75
+ chain_code_hex = hmac[64..-1]
76
+ if valid_private_key?(private_key_hex, curve)
77
+ from_hex(private_key_hex, chain_code_hex, curve)
78
+ else
79
+ from_seed(hmac, curve)
80
+ end
81
+ end
82
+
83
+ # Checks private key for a particular curve (used internally).
84
+ # Primarily checks whether key value is less than order of the curve.
85
+ #
86
+ # @param private_key_hex [String] private key hex string
87
+ # @param curve [Symbol] curve (<code>:secp256k1</code>, <code>:nist256p1</code> or <code>:ed25519</code>)
88
+ # @return [true, false] whether the key is valid
89
+ def self.valid_private_key?(private_key_hex, curve = :secp256k1)
90
+ return false unless private_key_hex =~ /\A[a-f0-9]{64}\z/
91
+
92
+ largest_key =
93
+ case curve
94
+ when :secp256k1
95
+ SECP256K1_LARGEST_KEY
96
+ when :nist256p1
97
+ NIST256P1_LARGEST_KEY
98
+ when :ed25519
99
+ ED25519_LARGEST_KEY
100
+ end
101
+ private_key = private_key_hex.to_i(16)
102
+ private_key <= largest_key && (private_key > 0 || curve == :ed25519)
103
+ end
104
+
105
+ # Creates private key from binary data.
106
+ # Use {.from_hex} or {.from_seed} instead for convenience.
107
+ #
108
+ # @param private_key [String] private key (in binary format)
109
+ # @param chain_code [String] chain_code (in binary format)
110
+ # @param curve [Symbol] curve (<code>:secp256k1</code>, <code>:nist256p1</code> or <code>:ed25519</code>)
111
+ def initialize(private_key, chain_code, curve = :secp256k1)
112
+ @private_key = private_key.dup.freeze
113
+ @chain_code = chain_code.dup.freeze
114
+
115
+ unless %i[secp256k1 nist256p1 ed25519].include?(curve)
116
+ raise ArgumentError.new('curve must be :secp256k1, :nist256p1 or :ed25519')
117
+ end
118
+
119
+ @curve = curve
120
+ end
121
+
122
+ # Compares with another {Key}.
123
+ #
124
+ # @param other [Key] subject of comparison
125
+ # @return [true, false] whether {private_key}, {chain_code} and {curve} of
126
+ # <code>other</code> matches <code>self</code>
127
+ def ==(other)
128
+ return false unless %i[private_key chain_code curve].all? { |m| other.respond_to?(m) }
129
+
130
+ private_key == other.private_key &&
131
+ chain_code == other.chain_code &&
132
+ curve == other.curve
133
+ end
134
+
135
+ # Chain code as hex string.
136
+ #
137
+ # @return [String]
138
+ def chain_code_hex
139
+ @chain_code_hex ||= @chain_code.to_hex.freeze
140
+ end
141
+
142
+ # {https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#key-identifiers BIP32 fingerprint}
143
+ # (first 4 bytes of HASH160 of public key) as hex string.
144
+ #
145
+ # @return [String]
146
+ def fingerprint
147
+ @fingerprint ||= public_key.hash160[0..3].to_hex
148
+ end
149
+
150
+ # Private key as hex string.
151
+ #
152
+ # @return [String]
153
+ def private_key_hex
154
+ @private_key_hex ||= @private_key.to_hex.freeze
155
+ end
156
+
157
+ # Public key (in binary format).
158
+ # Use {public_key_hex} for convenience.
159
+ #
160
+ # @return [String]
161
+ def public_key
162
+ @public_key ||= begin
163
+ case curve
164
+ when :ed25519
165
+ openssl_pkey.
166
+ public_to_der.
167
+ to_hex.
168
+ delete_prefix(ED25519_DER_PUBLIC_PREFIX).
169
+ from_hex.freeze
170
+ else
171
+ openssl_pkey.
172
+ public_key.
173
+ to_octet_string(:compressed).freeze
174
+ end
175
+ end
176
+ end
177
+
178
+ # Public key as hex string.
179
+ #
180
+ # @return [String]
181
+ def public_key_hex
182
+ @public_key_hex ||= public_key.to_hex.freeze
183
+ end
184
+
185
+ # Derive child key.
186
+ #
187
+ # @param path [String] derivation path, e.g. <code>'m/0/1'</code>,
188
+ # <code>'5'</code>, <code>'0/3'</code>, <code>"m/0/3'/5'/1/2"</code>.
189
+ # Use <code>'</code> for hardened keys.
190
+ def derive(path)
191
+ return self if path == 'm' || path.empty?
192
+ path = path.delete_prefix('m').delete_prefix('/')
193
+ path = path.split('/')
194
+
195
+ i_string = path.first
196
+ unless i_string =~ /\A[0-9]+'?\z/
197
+ raise ArgumentError.new("Wrong derivation segment: #{i_string.inspect}")
198
+ end
199
+ i = i_string.to_i
200
+ i += 2**31 if i_string[-1] == "'"
201
+
202
+ if curve == :ed25519 && i < 2**31
203
+ raise ArgumentError.new("Only hardened derivation supported with ED25519, got #{i_string} instead")
204
+ end
205
+
206
+ if curve != :ed25519
207
+ generator = openssl_group.generator
208
+ end
209
+
210
+ data =
211
+ if i >= 2**31
212
+ # Data for HMAC-SHA512(Key = cpar, Data = 0x00 || ser256(kpar) || ser32(i))
213
+ "00" + private_key_hex + ("%08x" % i)
214
+ else
215
+ # Data for HMAC-SHA512(Key = cpar, Data = serP(point(kpar)) || ser32(i))
216
+ generator.mul(private_key_hex.to_i(16)).to_octet_string(:compressed).to_hex + ("%08x" % i)
217
+ end
218
+ data = data.from_hex
219
+
220
+ hmac =
221
+ OpenSSL::HMAC.hexdigest(
222
+ "SHA512",
223
+ chain_code,
224
+ data
225
+ )
226
+ derived_private_key_hex = hmac[0..63]
227
+ derived_chain_code_hex = hmac[64..-1]
228
+
229
+ if curve != :ed25519
230
+ derived_private_key_hex, derived_chain_code_hex =
231
+ finish_derivation(derived_private_key_hex, derived_chain_code_hex, i)
232
+ end
233
+
234
+ new_key = self.class.from_hex(derived_private_key_hex, derived_chain_code_hex, curve)
235
+ new_key.derive(path[1..-1].join('/'))
236
+ end
237
+
238
+ # Exports key to PEM format.
239
+ #
240
+ # @return [String]
241
+ def to_pem
242
+ private_prefix =
243
+ case curve
244
+ when :secp256k1
245
+ SECP256K1_DER_PRIVATE_PREFIX
246
+ when :nist256p1
247
+ NIST256P1_DER_PRIVATE_PREFIX
248
+ when :ed25519
249
+ ED25519_DER_PRIVATE_PREFIX
250
+ end
251
+
252
+ der = (private_prefix + private_key_hex).from_hex
253
+ pem = <<~END
254
+ -----BEGIN PRIVATE KEY-----
255
+ #{der.to_base64.strip}
256
+ -----END PRIVATE KEY-----
257
+ END
258
+ end
259
+
260
+ # Creates {OpenSSL::PKey}[https://ruby-doc.org/stdlib-2.7.4/libdoc/openssl/rdoc/OpenSSL/PKey/EC/Point.html] instance.
261
+ #
262
+ # @return [OpenSSL::PKey::EC::Point]
263
+ def openssl_pkey
264
+ OpenSSL::PKey.read(to_pem)
265
+ end
266
+
267
+ # Creates {OpenSSL::PKey::EC::Group}[https://ruby-doc.org/stdlib-2.7.4/libdoc/openssl/rdoc/OpenSSL/PKey/EC/Group.html] instance for the key's {curve}.
268
+ #
269
+ # @return [OpenSSL::PKey::EC::Group]
270
+ def openssl_group
271
+ case curve
272
+ when :secp256k1
273
+ OpenSSL::PKey::EC::Group.new('secp256k1')
274
+ when :nist256p1
275
+ OpenSSL::PKey::EC::Group.new('prime256v1')
276
+ end
277
+ end
278
+
279
+ private
280
+
281
+ def finish_derivation(i_l_hex, i_r_hex, i)
282
+ group = openssl_group
283
+ i_l = i_l_hex.to_i(16)
284
+ new_key_int = (i_l + private_key_hex.to_i(16)) % group.order
285
+ if i_l >= group.order || new_key_int == 0
286
+ # let I = HMAC-SHA512(Key = cpar, Data = 0x01 || IR || ser32(i) and restart at step 2.
287
+ data = ("01" + i_r_hex + ("%08x" % i)).from_hex
288
+ hmac =
289
+ OpenSSL::HMAC.hexdigest(
290
+ "SHA512",
291
+ chain_code,
292
+ data
293
+ )
294
+ finish_derivation(hmac[0..63], hmac[64..-1], i)
295
+ else
296
+ ["%064x" % new_key_int, i_r_hex]
297
+ end
298
+ end
299
+ end
300
+ end