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.
- checksums.yaml +4 -4
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +14 -0
- data/README.md +26 -13
- data/lib/secp256k1/c.rb +73 -0
- data/lib/secp256k1/ecdh.rb +37 -0
- data/lib/secp256k1/ellswift.rb +25 -10
- data/lib/secp256k1/key.rb +342 -0
- data/lib/secp256k1/musig/key_agg.rb +74 -0
- data/lib/secp256k1/musig/session.rb +123 -0
- data/lib/secp256k1/musig.rb +203 -0
- data/lib/secp256k1/recovery.rb +38 -9
- data/lib/secp256k1/schnorrsig.rb +73 -11
- data/lib/secp256k1/version.rb +1 -1
- data/lib/secp256k1.rb +113 -33
- metadata +10 -6
|
@@ -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
|
data/lib/secp256k1/recovery.rb
CHANGED
|
@@ -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
|
-
|
|
11
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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
|
data/lib/secp256k1/schnorrsig.rb
CHANGED
|
@@ -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
|
|
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
|
-
|
|
12
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
38
|
-
|
|
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
|
-
|
|
50
|
-
result = secp256k1_schnorrsig_verify(context, signature,
|
|
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
|
data/lib/secp256k1/version.rb
CHANGED