secp256k1rb 0.1.1 → 0.3.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.
@@ -0,0 +1,74 @@
1
+ module Secp256k1
2
+ module MuSig
3
+
4
+ # Opaque data structure that caches information about public key aggregation.
5
+ class KeyAggCache < FFI::Struct
6
+ layout :data, [:uchar, 197]
7
+ end
8
+
9
+ # Key aggregation context class.
10
+ class KeyAggContext
11
+ include Secp256k1
12
+
13
+ attr_reader :cache
14
+
15
+ # Constructor.
16
+ # @param [Secp256k1::MuSig::KeyAggCache] key_agg_cache Key aggregation cache.
17
+ # @raise [ArgumentError] If invalid arguments specified.
18
+ def initialize(key_agg_cache)
19
+ raise ArgumentError, "key_agg_cache must be Secp256k1::KeyAggCache." unless key_agg_cache.is_a?(Secp256k1::KeyAggCache)
20
+ @cache = key_agg_cache
21
+ end
22
+
23
+ # Get aggregate public key.
24
+ # @return [String] An aggregated public key.
25
+ def aggregate_public_key
26
+ with_context do |context|
27
+ agg_pubkey = FFI::MemoryPointer.new(:uchar, 64)
28
+ if secp256k1_musig_pubkey_get(context, agg_pubkey, cache.pointer) == 0
29
+ raise Error, "secp256k1_musig_pubkey_get arguments invalid."
30
+ end
31
+ serialize_pubkey(context, agg_pubkey)
32
+ end
33
+ end
34
+
35
+ # Apply ordinary "EC" tweaking to a public key.
36
+ # @param [String] tweak Tweak value to tweak the aggregated key.
37
+ # @param [Boolean] xonly Apply x-only tweaking or not.
38
+ # @return [String] Tweaked x-only public key with hex format.
39
+ # @raise [ArgumentError] If invalid arguments specified.
40
+ # @raise [Secp256k1::Error]
41
+ def tweak_add(tweak, xonly: false)
42
+ validate_string!("tweak", tweak, 32)
43
+ with_context do |context|
44
+ tweak_ptr = FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, hex2bin(tweak))
45
+ pubkey_ptr = FFI::MemoryPointer.new(:uchar, 64)
46
+ if xonly
47
+ if secp256k1_musig_pubkey_xonly_tweak_add(context, pubkey_ptr, cache.pointer, tweak_ptr) == 0
48
+ raise Error, "secp256k1_musig_pubkey_tweak_add arguments invalid."
49
+ end
50
+ else
51
+ if secp256k1_musig_pubkey_ec_tweak_add(context, pubkey_ptr, cache.pointer, tweak_ptr) == 0
52
+ raise Error, "secp256k1_musig_pubkey_tweak_add arguments invalid."
53
+ end
54
+ end
55
+ serialize_pubkey(context, pubkey_ptr)
56
+ end
57
+ end
58
+
59
+ # Get KeyAggCache pointer.
60
+ # @return [FFI::MemoryPointer]
61
+ def pointer
62
+ cache.pointer
63
+ end
64
+
65
+ private
66
+
67
+ def serialize_pubkey(context, pubkey_ptr)
68
+ xonly = FFI::MemoryPointer.new(:uchar, 32)
69
+ secp256k1_xonly_pubkey_serialize(context, xonly, pubkey_ptr)
70
+ xonly.read_string(32).unpack1('H*')
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,123 @@
1
+ module Secp256k1
2
+ module MuSig
3
+ class Session
4
+ include Secp256k1
5
+ attr_reader :session
6
+ attr_reader :key_agg_ctx
7
+ attr_reader :agg_nonce
8
+ attr_reader :msg
9
+
10
+ # Create signing session.
11
+ # @param [Secp256k1::MuSig::KeyAggContext] key_agg_ctx The key aggregation context.
12
+ # @param [String] agg_nonce An aggregated public nonce.
13
+ # @param [String] msg The message to be signed.
14
+ # @raise [ArgumentError] If invalid arguments specified.
15
+ # @raise [Secp256k1::Error]
16
+ def initialize(key_agg_ctx, agg_nonce, msg)
17
+ raise ArgumentError, 'key_agg_ctx must be KeyAggContext.' unless key_agg_ctx.is_a?(KeyAggContext)
18
+ validate_string!('msg', msg, 32)
19
+ validate_string!('agg_nonce', agg_nonce, 66)
20
+ agg_nonce = hex2bin(agg_nonce)
21
+ msg = hex2bin(msg)
22
+ with_context do |context|
23
+ @session = FFI::MemoryPointer.new(:uchar, 133)
24
+ agg66 = FFI::MemoryPointer.new(:uchar, 66).put_bytes(0, agg_nonce)
25
+ agg_ptr = FFI::MemoryPointer.new(:uchar, 132)
26
+ if secp256k1_musig_aggnonce_parse(context, agg_ptr, agg66) == 0
27
+ raise Error, "secp256k1_musig_aggnonce_parse failed."
28
+ end
29
+ msg_ptr = FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, msg)
30
+ if secp256k1_musig_nonce_process(context, @session, agg_ptr, msg_ptr, key_agg_ctx.pointer) == 0
31
+ raise Error, "secp256k1_musig_nonce_process arguments invalid."
32
+ end
33
+ end
34
+ @agg_nonce = agg_nonce.unpack1('H*')
35
+ @msg = msg.unpack1('H*')
36
+ @key_agg_ctx = key_agg_ctx
37
+ end
38
+
39
+ # Produces a partial signature for a given key pair and secret nonce.
40
+ # @param [String] sec_nonce The secret nonce to sign the message.
41
+ # @param [String] private_key The private key to sign the message.
42
+ # @return [String] A partial signature.
43
+ # @raise [ArgumentError] If invalid arguments specified.
44
+ # @raise [Secp256k1::Error]
45
+ def partial_sign(sec_nonce, private_key)
46
+ raise ArgumentError, 'key_agg_ctx must be KeyAggContext.' unless key_agg_ctx.is_a?(KeyAggContext)
47
+ validate_string!('sec_nonce', sec_nonce, 132)
48
+ validate_string!('private_key', private_key, 32)
49
+ with_context do |context|
50
+ partial_sig = FFI::MemoryPointer.new(:uchar, 36)
51
+ sec_nonce_ptr = FFI::MemoryPointer.new(:uchar, 132).put_bytes(0, hex2bin(sec_nonce))
52
+ private_key_ptr = FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, hex2bin(private_key))
53
+ key_pair = FFI::MemoryPointer.new(:uchar, 86)
54
+ if secp256k1_keypair_create(context, key_pair, private_key_ptr) == 0
55
+ raise Error, "secp256k1_keypair_create invalid private_key."
56
+ end
57
+ if secp256k1_musig_partial_sign(
58
+ context, partial_sig, sec_nonce_ptr, key_pair, key_agg_ctx.cache.pointer, session) == 0
59
+ raise Error, "secp256k1_musig_partial_sign arguments invalid or sec_nonce has already been used for signing."
60
+ end
61
+ out32 = FFI::MemoryPointer.new(:uchar, 32)
62
+ secp256k1_musig_partial_sig_serialize(context, out32, partial_sig)
63
+ out32.read_string(32).unpack1('H*')
64
+ end
65
+ end
66
+
67
+ # Checks that an individual partial signature verifies.
68
+ # @param [String] partial_sig The partial signature to verify, sent by the signer associated with +pub_nonce+ and +public_key+.
69
+ # @param [String] pub_nonce The public nonce of the signer in the signing session.
70
+ # @param [String] public_key The public key of the signer in the signing session.
71
+ # @return [Boolean] The verification result.
72
+ # @raise [ArgumentError] If invalid arguments specified.
73
+ # @raise [Secp256k1::Error]
74
+ def verify_partial_sig(partial_sig, pub_nonce, public_key)
75
+ validate_string!('partial_sig', partial_sig, 32)
76
+ validate_string!('pub_nonce', pub_nonce, 66)
77
+ validate_string!('public_key', public_key, 33)
78
+ with_context do |context|
79
+ sig_ptr = parse_partial_sig(context, partial_sig)
80
+ public_key = FFI::MemoryPointer.new(:uchar, 33).put_bytes(0, hex2bin(public_key))
81
+ pubkey_ptr = FFI::MemoryPointer.new(:uchar, 64)
82
+ raise Error, "pubkey is invalid." unless secp256k1_ec_pubkey_parse(context, pubkey_ptr, public_key, 33) == 1
83
+ pub_nonce = FFI::MemoryPointer.new(:uchar, 66).put_bytes(0, hex2bin(pub_nonce))
84
+ nonce_ptr = FFI::MemoryPointer.new(:uchar, 132)
85
+ if secp256k1_musig_pubnonce_parse(context, nonce_ptr, pub_nonce) == 0
86
+ raise Error, "secp256k1_musig_pubnonce_parse failed."
87
+ end
88
+ secp256k1_musig_partial_sig_verify(context, sig_ptr, nonce_ptr, pubkey_ptr, key_agg_ctx.pointer, session) == 1
89
+ end
90
+ end
91
+
92
+ # Aggregates partial signatures
93
+ # @param [Array] partial_sigs Array of partial signatures.
94
+ # @return [String] An aggregated signature.
95
+ # @raise [ArgumentError] If invalid arguments specified.
96
+ # @raise [Secp256k1::Error]
97
+ def aggregate_partial_sigs(partial_sigs)
98
+ raise ArgumentError, "partial_sigs must be Array." unless partial_sigs.is_a?(Array)
99
+ raise ArgumentError, "partial_sigs must not be empty." if partial_sigs.empty?
100
+ with_context do |context|
101
+ sigs_ptr = FFI::MemoryPointer.new(:pointer, partial_sigs.length)
102
+ sigs_ptr.write_array_of_pointer(partial_sigs.map{|partial_sig| parse_partial_sig(context, partial_sig)})
103
+ sig64 = FFI::MemoryPointer.new(:uchar, 64)
104
+ if secp256k1_musig_partial_sig_agg(context, sig64, session, sigs_ptr, partial_sigs.length) == 0
105
+ raise Error, "secp256k1_musig_partial_sig_agg arguments invalid."
106
+ end
107
+ sig64.read_string(64).unpack1('H*')
108
+ end
109
+ end
110
+
111
+ private
112
+
113
+ def parse_partial_sig(context, partial_sig)
114
+ partial_sig = FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, hex2bin(partial_sig))
115
+ sig_ptr = FFI::MemoryPointer.new(:uchar, 36)
116
+ if secp256k1_musig_partial_sig_parse(context, sig_ptr, partial_sig) == 0
117
+ raise Error, "secp256k1_musig_partial_sig_parse failed."
118
+ end
119
+ sig_ptr
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,203 @@
1
+ require_relative 'musig/key_agg'
2
+ require_relative 'musig/session'
3
+
4
+ module Secp256k1
5
+
6
+ # MuSig module
7
+ # @example
8
+ # include Secp256k1
9
+ #
10
+ # # Two signer.
11
+ # sk1, pk1 = generate_key_pair
12
+ # sk2, pk2 = generate_key_pair
13
+ #
14
+ # # Aggregate public key
15
+ # key_agg_ctx = aggregate_pubkey([pk1, pk2])
16
+ # agg_pubkey = key_agg_ctx.aggregate_public_key
17
+ #
18
+ # # If you need tweak
19
+ # key_agg_ctx.tweak_add('7468697320636f756c64206265206120424950333220747765616b2e2e2e2e00')
20
+ # # If you need tweak with xonly
21
+ # key_agg_ctx.tweak_add('7468697320636f756c64206265206120546170726f6f7420747765616b2e2e00', xonly: true)
22
+ #
23
+ # agg_pubkey = key_agg_ctx.aggregate_public_key
24
+ #
25
+ # msg = Digest::SHA256.digest('message')
26
+ #
27
+ # # Nonce generation
28
+ # session_id1 = generate_musig_session_id
29
+ # secnonce1, pubnonce1 = target.generate_musig_nonce(
30
+ # session_id1,
31
+ # pk1,
32
+ # sk: sk1,
33
+ # key_agg_ctx: key_agg_ctx,
34
+ # msg: msg
35
+ # )
36
+ #
37
+ # session_id2 = generate_musig_session_id
38
+ # secnonce2, pubnonce2 = target.generate_musig_nonce(
39
+ # session_id2,
40
+ # pk2,
41
+ # sk: sk2,
42
+ # key_agg_ctx: key_agg_ctx,
43
+ # msg: msg
44
+ # )
45
+ #
46
+ # # Aggregate public nonces
47
+ # agg_nonce = aggregate_musig_nonce([pubnonce1, pubnonce2])
48
+ #
49
+ # # Generate partial sig
50
+ # musig_session = Secp256k1::MuSig::Session.new(key_agg_ctx, agg_nonce, msg)
51
+ # partial_sig1 = musig_session.partial_sign(secnonce1, sk1)
52
+ # partial_sig2 = musig_session.partial_sign(secnonce2, sk2)
53
+ #
54
+ # # Aggregate signature
55
+ # agg_sig = musig_session.aggregate_partial_sigs([partial_sig1, partial_sig2])
56
+ #
57
+ # # Verify schnorr signature
58
+ # verify_schnorr(msg, agg_sig, agg_pubkey)
59
+ module MuSig
60
+
61
+ # Aggregate public keys.
62
+ # @param [Array] pubkeys An array of public keys.
63
+ # @return [Secp2561k::MuSig::KeyAggContext]
64
+ # @raise [Secp256k1::Error]
65
+ def aggregate_pubkey(pubkeys)
66
+ raise ArgumentError, "pubkeys must be an array." unless pubkeys.is_a?(Array)
67
+ with_context do |context|
68
+ pubkeys_ptrs = pubkeys.map do |pubkey|
69
+ pubkey = hex2bin(pubkey)
70
+ validate_string!('pubkey', pubkey, 33)
71
+ input = FFI::MemoryPointer.new(:uchar, 33).put_bytes(0, pubkey)
72
+ pubkey_ptr = FFI::MemoryPointer.new(:uchar, 64)
73
+ raise Error, "pubkey is invalid." unless secp256k1_ec_pubkey_parse(context, pubkey_ptr, input, 33) == 1
74
+ pubkey_ptr
75
+ end
76
+ pubkeys_ptr = FFI::MemoryPointer.new(:pointer, pubkeys.length)
77
+ pubkeys_ptr.write_array_of_pointer(pubkeys_ptrs)
78
+ agg_pubkey = FFI::MemoryPointer.new(:uchar, 64)
79
+ cache = Secp256k1::MuSig::KeyAggCache.new
80
+ if secp256k1_musig_pubkey_agg(context, agg_pubkey, cache.pointer, pubkeys_ptr, pubkeys.length) == 0
81
+ raise Error, "secp256k1_musig_pubkey_agg argument error."
82
+ end
83
+ Secp256k1::MuSig::KeyAggContext.new(cache)
84
+ end
85
+ end
86
+
87
+ # Generate fresh session id for musig signing session.
88
+ # @return [String] The session id.
89
+ def generate_musig_session_id
90
+ SecureRandom.random_bytes(32).unpack1('H*')
91
+ end
92
+
93
+ # Generate nonce pair.
94
+ # @param [String] session_id The uniform random identifier for this session.
95
+ # @param [String] pk The public key for which the partial signature is generated.
96
+ # @param [String] sk (Optional) The private key for which the partial signature is generated.
97
+ # @param [Secp256k1::MuSig::KeyAggContext] key_agg_ctx (Optional) The aggregated public key context.
98
+ # @param [String] msg (Optional) The message to be signed.
99
+ # @param [String] extra_in (Optional) The auxiliary input.
100
+ # @return [Array(String)] The array of secret nonce and public nonce with hex format.
101
+ # @raise [ArgumentError] If invalid arguments specified.
102
+ # @raise [Secp256k1::Error]
103
+ def generate_musig_nonce(session_id, pk, sk: nil, key_agg_ctx: nil, msg: nil, extra_in: nil)
104
+ validate_string!("session_id", session_id, 32)
105
+ validate_string!("pk", pk, 33)
106
+ validate_string!("sk", sk, 32) if sk
107
+ validate_string!("msg", msg, 32) if msg
108
+ validate_string!("extra_in", extra_in, 32) if extra_in
109
+
110
+ if key_agg_ctx
111
+ raise ArgumentError, "key_agg must be Secp256k1::MuSig::KeyAggContext." unless key_agg_ctx.is_a?(KeyAggContext)
112
+ end
113
+
114
+ with_context do |context|
115
+ pk_ptr = FFI::MemoryPointer.new(:uchar, 33).put_bytes(0, hex2bin(pk))
116
+ pubkey = FFI::MemoryPointer.new(:uchar, 64)
117
+ raise Error, "pk is invalid public key." unless secp256k1_ec_pubkey_parse(context, pubkey, pk_ptr, 33) == 1
118
+
119
+ pubnonce = FFI::MemoryPointer.new(:uchar, 132)
120
+ secnonce = FFI::MemoryPointer.new(:uchar, 132)
121
+ seckey = sk ? FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, hex2bin(sk)) : nil
122
+ msg32 = msg ? FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, hex2bin(msg)) : nil
123
+ extra_input32 = extra_in ? FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, hex2bin(extra_in)) : nil
124
+ session_secrand32 = FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, hex2bin(session_id))
125
+
126
+ raise Error, "arguments is invalid." unless secp256k1_musig_nonce_gen(
127
+ context, secnonce, pubnonce, session_secrand32, seckey, pubkey, msg32, key_agg_ctx.pointer, extra_input32) == 1
128
+
129
+ pub66 = FFI::MemoryPointer.new(:uchar, 66)
130
+ secp256k1_musig_pubnonce_serialize(context, pub66, pubnonce)
131
+ [secnonce.read_string(132).unpack1('H*'), pub66.read_string(66).unpack1('H*')]
132
+ end
133
+ end
134
+
135
+ # Generate a nonce pair deterministically using a non-repeating counter instead of session randomness.
136
+ # The caller must ensure that +counter+ never repeats for the same key pair.
137
+ # @param [Integer] counter A non-repeating counter(uint64).
138
+ # @param [String] private_key The private key for which the partial signature is generated, with hex format.
139
+ # @param [Secp256k1::MuSig::KeyAggContext] key_agg_ctx (Optional) The aggregated public key context.
140
+ # @param [String] msg (Optional) The message to be signed.
141
+ # @param [String] extra_in (Optional) The auxiliary input.
142
+ # @return [Array(String)] The array of secret nonce and public nonce with hex format.
143
+ # @raise [ArgumentError] If invalid arguments specified.
144
+ # @raise [Secp256k1::Error]
145
+ def generate_musig_nonce_counter(counter, private_key, key_agg_ctx: nil, msg: nil, extra_in: nil)
146
+ raise ArgumentError, "counter must be Integer." unless counter.is_a?(Integer)
147
+ raise ArgumentError, "counter must be between 0 and 2**64-1." if counter < 0 || counter > (2**64 - 1)
148
+ validate_string!("private_key", private_key, 32)
149
+ validate_string!("msg", msg, 32) if msg
150
+ validate_string!("extra_in", extra_in, 32) if extra_in
151
+ if key_agg_ctx
152
+ raise ArgumentError, "key_agg_ctx must be Secp256k1::MuSig::KeyAggContext." unless key_agg_ctx.is_a?(KeyAggContext)
153
+ end
154
+
155
+ with_context do |context|
156
+ keypair = [create_keypair(private_key)].pack('H*')
157
+ keypair_ptr = FFI::MemoryPointer.new(:uchar, 96).put_bytes(0, keypair)
158
+ pubnonce = FFI::MemoryPointer.new(:uchar, 132)
159
+ secnonce = FFI::MemoryPointer.new(:uchar, 132)
160
+ msg32 = msg ? FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, hex2bin(msg)) : nil
161
+ extra_input32 = extra_in ? FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, hex2bin(extra_in)) : nil
162
+ cache_ptr = key_agg_ctx ? key_agg_ctx.pointer : nil
163
+
164
+ raise Error, "arguments is invalid." unless secp256k1_musig_nonce_gen_counter(
165
+ context, secnonce, pubnonce, counter, keypair_ptr, msg32, cache_ptr, extra_input32) == 1
166
+
167
+ pub66 = FFI::MemoryPointer.new(:uchar, 66)
168
+ secp256k1_musig_pubnonce_serialize(context, pub66, pubnonce)
169
+ [secnonce.read_string(132).unpack1('H*'), pub66.read_string(66).unpack1('H*')]
170
+ end
171
+ end
172
+
173
+ # Aggregates the nonces of all signers into a single nonce.
174
+ # @param [Array] pub_nonces An array of public nonces sent by the signers.
175
+ # @return [String] An aggregated public nonce.
176
+ # @raise [Secp256k1::Error]
177
+ # @raise [ArgumentError] If invalid arguments specified.
178
+ def aggregate_musig_nonce(pub_nonces)
179
+ raise ArgumentError, "pub_nonces must be Array." unless pub_nonces.is_a?(Array)
180
+
181
+ with_context do |context|
182
+ nonce_ptrs = pub_nonces.map do |pub_nonce|
183
+ pub_nonce = hex2bin(pub_nonce)
184
+ validate_string!("pub_nonce", pub_nonce, 66)
185
+ in66 = FFI::MemoryPointer.new(:uchar, 66).put_bytes(0, pub_nonce)
186
+ pub_nonce_ptr = FFI::MemoryPointer.new(:uchar, 132)
187
+ if secp256k1_musig_pubnonce_parse(context, pub_nonce_ptr, in66) == 0
188
+ raise Error, "secp256k1_musig_pubnonce_parse error."
189
+ end
190
+ pub_nonce_ptr
191
+ end
192
+ agg_nonce = FFI::MemoryPointer.new(:uchar, 132)
193
+ pubnonces = FFI::MemoryPointer.new(:pointer, pub_nonces.length)
194
+ pubnonces.write_array_of_pointer(nonce_ptrs)
195
+ result = secp256k1_musig_nonce_agg(context, agg_nonce, pubnonces, pub_nonces.length)
196
+ raise Error, "nonce aggregation failed." if result == 0
197
+ out66 = FFI::MemoryPointer.new(:uchar, 66)
198
+ secp256k1_musig_aggnonce_serialize(context, out66, agg_nonce)
199
+ out66.read_string(66).unpack1("H*")
200
+ end
201
+ end
202
+ end
203
+ end
@@ -1,4 +1,15 @@
1
1
  module Secp256k1
2
+ # Recover module
3
+ # @example
4
+ # include Secp256k1
5
+ #
6
+ # sk, _ = generate_key_pair
7
+ # msg = Digest::SHA256.digest('message')
8
+ # sig, rec = sign_recoverable(msg, sk)
9
+ # full_sig = [rec + 0x1b + 4].pack('C') + [sig].pack('H*')
10
+ # compressed = true
11
+ # recover_pubkey = target.recover(msg, full_sig, compressed)
12
+ #
2
13
  module Recover
3
14
  # Sign data with compact format.
4
15
  # @param [String] data The 32-byte message hash being signed.
@@ -7,13 +18,10 @@ module Secp256k1
7
18
  # @raise [Secp256k1::Error] If recovery failed.
8
19
  # @raise [ArgumentError] If invalid arguments specified.
9
20
  def sign_recoverable(data, private_key)
10
- raise ArgumentError, "private_key must be String." unless private_key.is_a?(String)
11
- raise ArgumentError, "data must by String." unless data.is_a?(String)
21
+ validate_string!("private_key", private_key, 32)
22
+ validate_string!("data", data, 32)
12
23
  private_key = hex2bin(private_key)
13
- raise ArgumentError, "private_key must be 32 bytes." unless private_key.bytesize == 32
14
24
  data = hex2bin(data)
15
- raise ArgumentError, "data must be 32 bytes." unless data.bytesize == 32
16
-
17
25
  with_context do |context|
18
26
  sig = FFI::MemoryPointer.new(:uchar, 65)
19
27
  hash =FFI::MemoryPointer.new(:uchar, data.bytesize).put_bytes(0, data)
@@ -39,12 +47,10 @@ module Secp256k1
39
47
  # @raise [Secp256k1::Error] If recover failed.
40
48
  # @raise [ArgumentError] If invalid arguments specified.
41
49
  def recover(data, signature, compressed)
42
- raise ArgumentError, "data must be String." unless data.is_a?(String)
43
- raise ArgumentError, "signature must be String." unless signature.is_a?(String)
50
+ validate_string!("data", data, 32)
51
+ validate_string!("signature", signature, 65)
44
52
  signature = hex2bin(signature)
45
- raise ArgumentError, "signature must be 65 bytes." unless signature.bytesize == 65
46
53
  data = hex2bin(data)
47
- raise ArgumentError, "data must be 32 bytes." unless data.bytesize == 32
48
54
  rec = (signature[0].ord - 0x1b) & 3
49
55
  raise ArgumentError, "rec must be between 0 and 3." if rec < 0 || rec > 3
50
56
 
@@ -62,5 +68,28 @@ module Secp256k1
62
68
  serialize_pubkey_internal(context, pubkey.read_string(64), compressed)
63
69
  end
64
70
  end
71
+
72
+ # Convert a recoverable signature into a normal DER-encoded ECDSA signature.
73
+ # @param [String] signature The recoverable signature with binary format(65 bytes), as accepted by {#recover}.
74
+ # @return [String] DER-encoded signature with binary format.
75
+ # @raise [Secp256k1::Error] If conversion failed.
76
+ # @raise [ArgumentError] If invalid arguments specified.
77
+ def recoverable_signature_to_ecdsa(signature)
78
+ validate_string!("signature", signature, 65)
79
+ signature = hex2bin(signature)
80
+ rec = (signature[0].ord - 0x1b) & 3
81
+ raise ArgumentError, "rec must be between 0 and 3." if rec < 0 || rec > 3
82
+ with_context do |context|
83
+ recoverable = FFI::MemoryPointer.new(:uchar, 65)
84
+ input = FFI::MemoryPointer.new(:uchar, 64).put_bytes(0, signature[1..-1])
85
+ raise Error, 'secp256k1_ecdsa_recoverable_signature_parse_compact failed.' unless secp256k1_ecdsa_recoverable_signature_parse_compact(context, recoverable, input, rec) == 1
86
+ internal_signature = FFI::MemoryPointer.new(:uchar, 64)
87
+ raise Error, 'secp256k1_ecdsa_recoverable_signature_convert failed.' unless secp256k1_ecdsa_recoverable_signature_convert(context, internal_signature, recoverable) == 1
88
+ output = FFI::MemoryPointer.new(:uchar, 72)
89
+ output_len = FFI::MemoryPointer.new(:uint64).put_uint64(0, 72)
90
+ raise Error, 'secp256k1_ecdsa_signature_serialize_der failed.' unless secp256k1_ecdsa_signature_serialize_der(context, output, output_len, internal_signature) == 1
91
+ output.read_string(output_len.read_uint64)
92
+ end
93
+ end
65
94
  end
66
95
  end
@@ -1,20 +1,29 @@
1
1
  module Secp256k1
2
+ # SchnorrSig module
3
+ # @example
4
+ # include Secp256k1
5
+ #
6
+ # sk, pk = generate_key_pair
7
+ #
8
+ # # sign and verify (Schnorr)
9
+ # signature = sign_schnorr(msg, sk)
10
+ # verify_schnorr(msg, signature, pk[2..-1]) # public key must be 32 bytes
11
+ #
2
12
  module SchnorrSig
3
13
 
4
14
  # Sign to data using schnorr.
5
15
  # @param [String] data The 32-byte message hash being signed with binary format.
6
16
  # @param [String] private_key a private key with hex format using sign.
7
- # @param [String] aux_rand a extra entropy.
17
+ # @param [String] aux_rand The 32-byte extra entropy.
8
18
  # @return [String] signature data with binary format. If unsupported algorithm specified, return nil.
9
19
  # @raise [ArgumentError] If invalid arguments specified.
10
20
  def sign_schnorr(data, private_key, aux_rand = nil)
11
- raise ArgumentError, "private_key must be String." unless private_key.is_a?(String)
12
- raise ArgumentError, "data must by String." unless data.is_a?(String)
21
+ validate_string!("data", data, 32)
22
+ validate_string!("private_key", private_key, 32)
23
+ validate_string!("aux_rand", aux_rand, 32) if aux_rand
13
24
  raise ArgumentError, "aux_rand must be String." if !aux_rand.nil? && !aux_rand.is_a?(String)
14
25
  private_key = hex2bin(private_key)
15
- raise ArgumentError, "private_key must be 32 bytes." unless private_key.bytesize == 32
16
26
  data = hex2bin(data)
17
- raise ArgumentError, "data must be 32 bytes." unless data.bytesize == 32
18
27
 
19
28
  with_context do |context|
20
29
  keypair = [create_keypair(private_key)].pack('H*')
@@ -27,18 +36,71 @@ module Secp256k1
27
36
  end
28
37
  end
29
38
 
30
- # Verify ecdsa signature.
39
+ # Magic bytes for secp256k1_schnorrsig_extraparams.
40
+ SCHNORRSIG_EXTRAPARAMS_MAGIC = [0xda, 0x6f, 0xb3, 0x8c].pack('C*').freeze
41
+
42
+ # Sign to a variable-length message using schnorr (BIP340 sign with custom parameters).
43
+ # @param [String] data The message being signed with binary format. Unlike {#sign_schnorr}, the length is arbitrary.
44
+ # @param [String] private_key a private key with hex format using sign.
45
+ # @param [String] aux_rand (Optional)The 32-byte extra entropy.
46
+ # @return [String] signature data with binary format(64 bytes).
47
+ # @raise [Secp256k1::Error] If signing failed.
48
+ # @raise [ArgumentError] If invalid arguments specified.
49
+ def sign_schnorr_custom(data, private_key, aux_rand = nil)
50
+ raise ArgumentError, "data must be String." unless data.is_a?(String)
51
+ validate_string!("private_key", private_key, 32)
52
+ validate_string!("aux_rand", aux_rand, 32) if aux_rand
53
+ private_key = hex2bin(private_key)
54
+ data = hex2bin(data)
55
+ aux_rand = hex2bin(aux_rand) if aux_rand
56
+
57
+ with_context do |context|
58
+ keypair = [create_keypair(private_key)].pack('H*')
59
+ keypair = FFI::MemoryPointer.new(:uchar, 96).put_bytes(0, keypair)
60
+ signature = FFI::MemoryPointer.new(:uchar, 64)
61
+ msg = FFI::MemoryPointer.new(:uchar, [data.bytesize, 1].max).put_bytes(0, data)
62
+
63
+ # Build secp256k1_schnorrsig_extraparams: magic[4], noncefp(NULL=default BIP340), ndata(aux_rand or NULL).
64
+ ptr_size = FFI::Pointer.size
65
+ extraparams = FFI::MemoryPointer.new(:uchar, ptr_size * 3)
66
+ extraparams.put_bytes(0, SCHNORRSIG_EXTRAPARAMS_MAGIC)
67
+ extraparams.put_pointer(ptr_size, FFI::Pointer::NULL)
68
+ ndata = aux_rand ? FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, aux_rand) : FFI::Pointer::NULL
69
+ extraparams.put_pointer(ptr_size * 2, ndata)
70
+
71
+ raise Error, 'Failed to generate schnorr signature.' unless secp256k1_schnorrsig_sign_custom(context, signature, msg, data.bytesize, keypair, extraparams) == 1
72
+ signature.read_string(64)
73
+ end
74
+ end
75
+
76
+ # Verify schnorr signature.
31
77
  # @param [String] data The 32-byte message hash assumed to be signed.
32
78
  # @param [String] signature signature data with binary format
33
79
  # @param [String] pubkey a public key with hex format using verify.
34
80
  # @return [Boolean] verification result.
35
81
  # @raise [ArgumentError] If invalid arguments specified.
36
82
  def verify_schnorr(data, signature, pubkey)
37
- raise ArgumentError, "sig must be String." unless signature.is_a?(String)
38
- raise ArgumentError, "pubkey must be String." unless pubkey.is_a?(String)
83
+ validate_string!("data", data, 32)
84
+ verify_schnorr_internal(data, signature, pubkey)
85
+ end
86
+
87
+ # Verify a schnorr signature over a variable-length message (counterpart of {#sign_schnorr_custom}).
88
+ # @param [String] data The message assumed to be signed. The length is arbitrary.
89
+ # @param [String] signature signature data with binary format(64 bytes).
90
+ # @param [String] pubkey an x-only public key with hex format(32 bytes) using verify.
91
+ # @return [Boolean] verification result.
92
+ # @raise [ArgumentError] If invalid arguments specified.
93
+ def verify_schnorr_custom(data, signature, pubkey)
39
94
  raise ArgumentError, "data must be String." unless data.is_a?(String)
95
+ verify_schnorr_internal(data, signature, pubkey)
96
+ end
97
+
98
+ private
99
+
100
+ def verify_schnorr_internal(data, signature, pubkey)
101
+ validate_string!("signature", signature, 64)
102
+ validate_string!("pubkey", pubkey, 32)
40
103
  data = hex2bin(data)
41
- raise ArgumentError, "data must be 32 bytes." unless data.bytesize == 32
42
104
  pubkey = hex2bin(pubkey)
43
105
  signature = hex2bin(signature)
44
106
  with_context do |context|
@@ -46,8 +108,8 @@ module Secp256k1
46
108
  pubkey = [full_pubkey_from_xonly_pubkey(pubkey)].pack('H*')
47
109
  xonly_pubkey = FFI::MemoryPointer.new(:uchar, pubkey.bytesize).put_bytes(0, pubkey)
48
110
  signature = FFI::MemoryPointer.new(:uchar, signature.bytesize).put_bytes(0, signature)
49
- msg32 = FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, data)
50
- result = secp256k1_schnorrsig_verify(context, signature, msg32, 32, xonly_pubkey)
111
+ msg = FFI::MemoryPointer.new(:uchar, [data.bytesize, 1].max).put_bytes(0, data)
112
+ result = secp256k1_schnorrsig_verify(context, signature, msg, data.bytesize, xonly_pubkey)
51
113
  result == 1
52
114
  end
53
115
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Secp256k1
4
- VERSION = "0.1.1"
4
+ VERSION = "0.3.0"
5
5
  end