schnorr 0.2.0 → 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/.travis.yml +1 -0
- data/README.md +54 -2
- data/lib/schnorr.rb +27 -1
- data/lib/schnorr/mu_sig.rb +84 -0
- data/lib/schnorr/mu_sig/session.rb +51 -0
- data/lib/schnorr/version.rb +1 -1
- metadata +5 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 552fd9baac0e3e4242003e0844c1e5d08dc69f46bf6b605f59293f9793c2c8a1
|
4
|
+
data.tar.gz: 4de25e1831dc171450435256e4e15f5734bfa8da522c57b5ffb90c9d0f1c32f9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dcff61f96ee167a43e31fa71b196c118ae8471c949271282c8d0f7b7bee3cc0ce94323d24e70c43619fdd3571f0084e652c1d251e0c07c59deccf63155757085
|
7
|
+
data.tar.gz: 891acabe7a536901ae0209798df11a6f3031dc90e534446ac8982a225ce1ce21c1d011d4e4378942af7cd52c11962082690c4bd51830d604fb0ff31d1a057391
|
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -98,9 +98,61 @@ Schnorr.valid_sig?(message, public_key, signature, group: ECDSA::Group::xxx)
|
|
98
98
|
|
99
99
|
Note: But this library has only been tested with `secp256k1`. So another curve are not tested.
|
100
100
|
|
101
|
+
### MuSig
|
102
|
+
|
103
|
+
The MuSig signature scheme is based on the implementation of the
|
104
|
+
[secp256k1-zkp](https://github.com/ElementsProject/secp256k1-zkp/blob/secp256k1-zkp/src/modules/musig/musig.md)
|
105
|
+
and [bip-schnorr](https://github.com/guggero/bip-schnorr).
|
106
|
+
|
107
|
+
Note: In this scheme, this library only supports secp256k1 curve.
|
108
|
+
|
109
|
+
```ruby
|
110
|
+
require 'schnorr'
|
111
|
+
|
112
|
+
# Key generation
|
113
|
+
## First, MuSig participants must compute combined public key.
|
114
|
+
|
115
|
+
combined_pubkey = Schnorr::MuSig.pubkey_combine(pubkeys) # pubkeys is an array of public key with binary format.
|
116
|
+
|
117
|
+
## combined_pubkey is the point which added the point which performed the following multiplication to each participant's public key.
|
118
|
+
## SHA256(TAG || TAG || ell || pubkey index) * Participant's Pubkey Point
|
119
|
+
## ell is calculated by SHA256(pubkey1 + pubkey2 + .... pubkeyn)
|
120
|
+
|
121
|
+
ell = Schnorr::MuSig.compute_ell(pubkeys)
|
122
|
+
|
123
|
+
# Signing participant
|
124
|
+
|
125
|
+
## the signer create new session
|
126
|
+
session = Schnorr::MuSig.session_initialize(nil, private_key, message, combined_pubkey, ell, index) # index = 0
|
127
|
+
|
128
|
+
## each participant use same session id
|
129
|
+
session_id = session.id
|
130
|
+
|
131
|
+
## each participant create own session.
|
132
|
+
session = Schnorr::MuSig.session_initialize(session_id, private_key, message, combined_pubkey, ell, index)
|
133
|
+
|
134
|
+
## participant send his nonce and collect other participant's nonce.
|
135
|
+
session.nonce
|
136
|
+
|
137
|
+
other_nonces = [...]
|
138
|
+
|
139
|
+
## If collect all participant's nonce, then calculate combined nonce.
|
140
|
+
## In this method, if jacobi(y(combined_point)) != 1,
|
141
|
+
## combined_point changed to combined_point.negate and session#nonce_negate changed to true.
|
142
|
+
combined_nonce = session.nonce_combine(other_nonces)
|
143
|
+
|
144
|
+
## each participants create partial signature
|
145
|
+
partial_sig = session.partial_sign(message, combined_nonce, combined_pubkey)
|
146
|
+
|
147
|
+
## Aggregate signature.
|
148
|
+
signature = Schnorr::MuSig.partial_sig_combine(combined_nonce, signatures)
|
149
|
+
|
150
|
+
## Verify. If signature is valid, following method will return true.
|
151
|
+
Schnorr.valid_sig?(message, combined_pubkey, signature.encode)
|
152
|
+
```
|
153
|
+
|
101
154
|
## TODO
|
102
155
|
|
103
156
|
The following is unimplemented now.
|
104
157
|
|
105
|
-
*
|
106
|
-
* MuSig
|
158
|
+
* (t, n) threshold signature.
|
data/lib/schnorr.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'ecdsa'
|
2
2
|
require 'securerandom'
|
3
3
|
require_relative 'schnorr/signature'
|
4
|
+
require_relative 'schnorr/mu_sig'
|
4
5
|
|
5
6
|
module Schnorr
|
6
7
|
|
@@ -115,6 +116,31 @@ module Schnorr
|
|
115
116
|
field.mod(ECDSA.normalize_digest(Digest::SHA256.digest(r + public_key + message), group.bit_length))
|
116
117
|
end
|
117
118
|
|
118
|
-
|
119
|
+
class ::Integer
|
120
|
+
|
121
|
+
def to_hex
|
122
|
+
hex = to_s(16)
|
123
|
+
hex.rjust((hex.length / 2.0).ceil * 2, '0')
|
124
|
+
end
|
125
|
+
|
126
|
+
def method_missing(method, *args)
|
127
|
+
return mod_pow(args[0], args[1]) if method == :pow && args.length < 3
|
128
|
+
super
|
129
|
+
end
|
130
|
+
|
131
|
+
# alternative implementation of Integer#pow for ruby 2.4 and earlier.
|
132
|
+
def mod_pow(x, y)
|
133
|
+
return self ** x unless y
|
134
|
+
b = self
|
135
|
+
result = 1
|
136
|
+
while x > 0
|
137
|
+
result = (result * b) % y if (x & 1) == 1
|
138
|
+
x >>= 1
|
139
|
+
b = (b * b) % y
|
140
|
+
end
|
141
|
+
result
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
119
145
|
|
120
146
|
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require_relative 'mu_sig/session'
|
2
|
+
|
3
|
+
module Schnorr
|
4
|
+
|
5
|
+
# https://github.com/ElementsProject/secp256k1-zkp/blob/secp256k1-zkp/src/modules/musig/musig.md
|
6
|
+
module MuSig
|
7
|
+
|
8
|
+
# SHA256("MuSig coefficient")
|
9
|
+
TAG = ['74894a2bec01af68225002cae9b0430ab63151ede5d31f641791976c7140b57b'].pack('H*')
|
10
|
+
|
11
|
+
module_function
|
12
|
+
|
13
|
+
# Computes ell = SHA256(pk[0], ..., pk[np-1])
|
14
|
+
# @param public_keys (Array[String]) The set of public keys with binary format.
|
15
|
+
# @return (String) ell
|
16
|
+
def compute_ell(public_keys)
|
17
|
+
Digest::SHA256.digest(public_keys.join)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Combine public keys.
|
21
|
+
# @param public_keys (Array[String]) The set of public keys with binary format.
|
22
|
+
# @return (String) a combined public key point with binary format.
|
23
|
+
def pubkey_combine(public_keys, ell: nil)
|
24
|
+
ell = compute_ell(public_keys) unless ell
|
25
|
+
points = public_keys.map.with_index do |p, idx|
|
26
|
+
xi = ECDSA::Format::PointOctetString.decode(p, ECDSA::Group::Secp256k1)
|
27
|
+
xi.multiply_by_scalar(coefficient(ell, idx))
|
28
|
+
end
|
29
|
+
sum = points.inject(:+)
|
30
|
+
ECDSA::Format::PointOctetString.encode(sum, compression: true)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Computes MuSig coefficient SHA256(TAG || TAG || ++ell++ || ++idx++).
|
34
|
+
# @param ell (String) a ell with binary format.
|
35
|
+
# @param idx (Integer) an index of public key.
|
36
|
+
# @return (Integer) coefficient value.
|
37
|
+
def coefficient(ell, idx)
|
38
|
+
field = ECDSA::PrimeField.new(ECDSA::Group::Secp256k1.order)
|
39
|
+
field.mod(Digest::SHA256.digest(TAG + TAG + ell + [idx].pack('I*')).unpack('H*').first.to_i(16))
|
40
|
+
end
|
41
|
+
|
42
|
+
# Initialize session to signer starts the session.
|
43
|
+
# @param session_id (String) if ++session_id++ is nil, generate new one.
|
44
|
+
# @param private_key (Integer) a private key.
|
45
|
+
# @param message (String) a message for sign with binary format.
|
46
|
+
# @param combined_pubkey (String) combined public key with binary format.
|
47
|
+
# @param ell (String) ell with binary format.
|
48
|
+
# @param index (Integer) public key index.
|
49
|
+
# @return (Schnorr::MuSig::Session) session object.
|
50
|
+
def session_initialize(session_id, private_key, message, combined_pubkey, ell, index)
|
51
|
+
raise ArgumentError, 'session_id must be 32 bytes.' if session_id && session_id.bytesize != 32
|
52
|
+
raise ArgumentError, 'message must be 32 bytes.' unless message.bytesize == 32
|
53
|
+
raise ArgumentError, 'combined_pubkey must be 33 bytes.' unless combined_pubkey.bytesize == 33
|
54
|
+
raise ArgumentError, 'ell must be 32 bytes.' unless ell.bytesize == 32
|
55
|
+
secret = ECDSA::Format::IntegerOctetString.encode(private_key, ECDSA::Group::Secp256k1.byte_length)
|
56
|
+
|
57
|
+
field = ECDSA::PrimeField.new(ECDSA::Group::Secp256k1.order)
|
58
|
+
session = Schnorr::MuSig::Session.new(session_id)
|
59
|
+
coefficient = coefficient(ell, index)
|
60
|
+
session.secret_key = field.mod(private_key * coefficient)
|
61
|
+
session.secret_nonce = Digest::SHA256.digest(session.id + message + combined_pubkey + secret).unpack('H*').first.to_i(16)
|
62
|
+
raise ArgumentError, 'secret nonce must be an integer in the ragen 1..n-1' unless field.include?(session.secret_nonce)
|
63
|
+
point_r = ECDSA::Group::Secp256k1.new_point(session.secret_nonce)
|
64
|
+
session.nonce = ECDSA::Format::PointOctetString.encode(point_r, compression: true)
|
65
|
+
session.commitment = Digest::SHA256.digest(session.nonce)
|
66
|
+
session
|
67
|
+
end
|
68
|
+
|
69
|
+
# Combine the partial signatures to obtain a complete signature.
|
70
|
+
# @param combined_nonce (Array)
|
71
|
+
# @param signatures (Array) co-signer's signature.
|
72
|
+
# @return (Schnorr::Signature) a combined signature.
|
73
|
+
def partial_sig_combine(combined_nonce, signatures)
|
74
|
+
point_r = ECDSA::Format::PointOctetString.decode(combined_nonce, ECDSA::Group::Secp256k1)
|
75
|
+
field = ECDSA::PrimeField.new(ECDSA::Group::Secp256k1.order)
|
76
|
+
signature = signatures.inject{|sum, s|field.mod(sum + s)}
|
77
|
+
Schnorr::Signature.new(point_r.x, signature)
|
78
|
+
end
|
79
|
+
|
80
|
+
private_class_method :coefficient
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Schnorr
|
2
|
+
module MuSig
|
3
|
+
class Session
|
4
|
+
|
5
|
+
attr_reader :id # binary
|
6
|
+
attr_accessor :secret_key # Integer
|
7
|
+
attr_accessor :secret_nonce # Integer
|
8
|
+
attr_accessor :nonce # binary
|
9
|
+
attr_accessor :commitment # binary
|
10
|
+
attr_accessor :nonce_negate # Boolean
|
11
|
+
|
12
|
+
def initialize(session_id = SecureRandom.random_bytes(32))
|
13
|
+
@id = session_id
|
14
|
+
@nonce_negate = false
|
15
|
+
end
|
16
|
+
|
17
|
+
def nonce_negate?
|
18
|
+
@nonce_negate
|
19
|
+
end
|
20
|
+
|
21
|
+
# Combine nonce
|
22
|
+
# @param nonces (Array[String]) an array of other signer's nonce with binary format.
|
23
|
+
# @return (String) combined nonce with binary format.
|
24
|
+
def nonce_combine(nonces)
|
25
|
+
points = ([nonce]+ nonces).map.with_index {|n, index|ECDSA::Format::PointOctetString.decode(n, ECDSA::Group::Secp256k1)}
|
26
|
+
r_point = points.inject(:+)
|
27
|
+
unless ECDSA::PrimeField.jacobi(r_point.y, ECDSA::Group::Secp256k1.field.prime) == 1
|
28
|
+
self.nonce_negate = true
|
29
|
+
r_point = r_point.negate
|
30
|
+
end
|
31
|
+
ECDSA::Format::PointOctetString.encode(r_point, compression: true)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Compute partial signature.
|
35
|
+
# @param message (String) a message for signature with binary format.
|
36
|
+
# @param combined_nonce (String) combined nonce with binary format.
|
37
|
+
# @param combined_pubkey (String) combined public key with binary format.
|
38
|
+
# @return (Integer) a partial signature.
|
39
|
+
def partial_sign(message, combined_nonce, combined_pubkey)
|
40
|
+
field = ECDSA::PrimeField.new(ECDSA::Group::Secp256k1.order)
|
41
|
+
point_r = ECDSA::Format::PointOctetString.decode(combined_nonce, ECDSA::Group::Secp256k1)
|
42
|
+
point_p = ECDSA::Format::PointOctetString.decode(combined_pubkey, ECDSA::Group::Secp256k1)
|
43
|
+
e = Schnorr.create_challenge(point_r.x, point_p, message, field, point_r.group)
|
44
|
+
k = secret_nonce
|
45
|
+
k = 0 - k if nonce_negate?
|
46
|
+
field.mod(secret_key * e + k)
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/lib/schnorr/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: schnorr
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- azuchi
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-04-
|
11
|
+
date: 2019-04-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: ecdsa
|
@@ -86,6 +86,8 @@ files:
|
|
86
86
|
- bin/console
|
87
87
|
- bin/setup
|
88
88
|
- lib/schnorr.rb
|
89
|
+
- lib/schnorr/mu_sig.rb
|
90
|
+
- lib/schnorr/mu_sig/session.rb
|
89
91
|
- lib/schnorr/signature.rb
|
90
92
|
- lib/schnorr/version.rb
|
91
93
|
- schnorrrb.gemspec
|
@@ -108,8 +110,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
108
110
|
- !ruby/object:Gem::Version
|
109
111
|
version: '0'
|
110
112
|
requirements: []
|
111
|
-
|
112
|
-
rubygems_version: 2.7.8
|
113
|
+
rubygems_version: 3.0.3
|
113
114
|
signing_key:
|
114
115
|
specification_version: 4
|
115
116
|
summary: The ruby implementation of Schnorr signature.
|