wots 0.1.0 → 0.2.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-version +1 -1
- data/README.md +21 -1
- data/lib/wots/address.rb +54 -13
- data/lib/wots/param.rb +80 -21
- data/lib/wots/private_key.rb +17 -6
- data/lib/wots/public_key.rb +6 -4
- data/lib/wots/util.rb +12 -10
- data/lib/wots/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: af0b30e037fbb6e5727fa6f90a7c601625d9a959268b28e7d48bb92ab5db1b37
|
|
4
|
+
data.tar.gz: 5af9c89ca3b088bef40f3f6c1307a4c8fc9c108d54967ad5670118b0e87730bc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f4171a56cc4b0245e5e5b2f73951a8a5f2d743d488161bc5d7775370a77e1ba0e992d928a933cb30cd7657ed494d65c8864ae17ba79ac1cad4fc0838b70b9ec3
|
|
7
|
+
data.tar.gz: '009254475b5624eabe23d3f7d96a050323c36d0894c911f078a7cb7c3c4dd1cae000fb3683637441b8d2b242ff6e2678b1adb9e6c08a301995712ea0eee38dd8'
|
data/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
ruby-
|
|
1
|
+
ruby-4.0.0
|
data/README.md
CHANGED
|
@@ -72,14 +72,34 @@ param = WOTS::Param::SHA512
|
|
|
72
72
|
param = WOTS::Param::SHAKE256
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
+
## Input format
|
|
76
|
+
|
|
77
|
+
All key material, seeds and messages are **even-length hexadecimal strings**. Raw binary strings and
|
|
78
|
+
odd-length hex strings are rejected with an `ArgumentError`; convert them with `unpack1('H*')` first.
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
message = Digest::SHA256.hexdigest("some payload") # 64 hex chars = 32 bytes
|
|
82
|
+
signature = private_key.sign(pub_seed, message)
|
|
83
|
+
```
|
|
84
|
+
|
|
75
85
|
## Security Considerations
|
|
76
86
|
|
|
77
|
-
**⚠️ IMPORTANT: This is a
|
|
87
|
+
**⚠️ IMPORTANT: This is a cryptographic library implementation. While it follows RFC 8391 specifications,
|
|
78
88
|
it has not undergone formal security audits. Use at your own risk in production environments.**
|
|
79
89
|
|
|
80
90
|
WOTS+ is a **one-time signature scheme**. Each private key should only be used to sign **one message**.
|
|
81
91
|
Reusing a private key to sign multiple messages can leak information about the private key and compromise security.
|
|
82
92
|
|
|
93
|
+
The seed passed to `WOTS::PrivateKey.from_seed` is secret key material and must come from a
|
|
94
|
+
cryptographically secure random number generator, e.g. `SecureRandom.hex(param.n)`.
|
|
95
|
+
|
|
96
|
+
Only the Winternitz parameters defined in RFC 8391 (`w = 4` and `w = 16`) are accepted. Other values
|
|
97
|
+
overflow the checksum encoding, which drops the high bits of the checksum and allows an attacker to
|
|
98
|
+
forge signatures without the private key.
|
|
99
|
+
|
|
100
|
+
Private key material is held in Ruby `String` objects and is not zeroized; it remains in memory until
|
|
101
|
+
garbage collected.
|
|
102
|
+
|
|
83
103
|
## Specifications
|
|
84
104
|
|
|
85
105
|
This implementation follows [RFC 8391 - XMSS: eXtended Merkle Signature Scheme](https://datatracker.ietf.org/doc/html/rfc8391), Section 3 (WOTS+).
|
data/lib/wots/address.rb
CHANGED
|
@@ -1,19 +1,50 @@
|
|
|
1
1
|
module WOTS
|
|
2
|
+
# WOTS+ hash function address.
|
|
3
|
+
# Each field is packed into a fixed width big endian integer by #to_payload,
|
|
4
|
+
# so a value outside its range would silently wrap and collide with another
|
|
5
|
+
# address, breaking the domain separation the chaining function relies on.
|
|
2
6
|
class Address
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
UINT32_MAX = 0xFFFFFFFF
|
|
8
|
+
UINT64_MAX = 0xFFFFFFFFFFFFFFFF
|
|
9
|
+
|
|
10
|
+
attr_reader :layer_addr
|
|
11
|
+
attr_reader :tree_addr
|
|
12
|
+
attr_reader :ots_addr
|
|
13
|
+
attr_reader :chain_addr
|
|
14
|
+
attr_reader :hash_addr
|
|
15
|
+
attr_reader :key_and_mask # 0: key generation, 1: bitmask generation
|
|
9
16
|
|
|
10
17
|
def initialize(layer_addr: 0, tree_addr: 0, ots_addr: 0, chain_addr: 0, hash_addr: 0, key_and_mask: 0)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
self.layer_addr = layer_addr
|
|
19
|
+
self.tree_addr = tree_addr
|
|
20
|
+
self.ots_addr = ots_addr
|
|
21
|
+
self.chain_addr = chain_addr
|
|
22
|
+
self.hash_addr = hash_addr
|
|
23
|
+
self.key_and_mask = key_and_mask
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def layer_addr=(value)
|
|
27
|
+
@layer_addr = check_range(:layer_addr, value, UINT32_MAX)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def tree_addr=(value)
|
|
31
|
+
@tree_addr = check_range(:tree_addr, value, UINT64_MAX)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def ots_addr=(value)
|
|
35
|
+
@ots_addr = check_range(:ots_addr, value, UINT32_MAX)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def chain_addr=(value)
|
|
39
|
+
@chain_addr = check_range(:chain_addr, value, UINT32_MAX)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def hash_addr=(value)
|
|
43
|
+
@hash_addr = check_range(:hash_addr, value, UINT32_MAX)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def key_and_mask=(value)
|
|
47
|
+
@key_and_mask = check_range(:key_and_mask, value, UINT32_MAX)
|
|
17
48
|
end
|
|
18
49
|
|
|
19
50
|
def type
|
|
@@ -23,5 +54,15 @@ module WOTS
|
|
|
23
54
|
def to_payload
|
|
24
55
|
[layer_addr, tree_addr, type, ots_addr, chain_addr, hash_addr, key_and_mask].pack('NQ>NNNNN')
|
|
25
56
|
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
# @raise ArgumentError If +value+ would not survive the round trip through +pack+.
|
|
61
|
+
def check_range(name, value, max)
|
|
62
|
+
raise ArgumentError, "#{name} must be integer." unless value.is_a?(Integer)
|
|
63
|
+
raise ArgumentError, "#{name} must be between 0 and #{max}." unless value >= 0 && value <= max
|
|
64
|
+
|
|
65
|
+
value
|
|
66
|
+
end
|
|
26
67
|
end
|
|
27
|
-
end
|
|
68
|
+
end
|
data/lib/wots/param.rb
CHANGED
|
@@ -10,25 +10,48 @@ module WOTS
|
|
|
10
10
|
autoload :SHA512, 'wots/param/sha512'
|
|
11
11
|
autoload :SHAKE256, 'wots/param/shake256'
|
|
12
12
|
|
|
13
|
+
# The parameter sets defined in RFC 8391 and the +n+ they require.
|
|
14
|
+
SUPPORTED_NAMES = {
|
|
15
|
+
'WOTSP-SHA2_256' => 32,
|
|
16
|
+
'WOTSP-SHA2_512' => 64,
|
|
17
|
+
'WOTSP-SHAKE_256' => 32
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
# The Winternitz parameter is a member of the set {4, 16}.
|
|
21
|
+
# Other values break the checksum encoding below and allow signature forgery.
|
|
22
|
+
SUPPORTED_W = [4, 16].freeze
|
|
23
|
+
|
|
13
24
|
attr_reader :name
|
|
14
25
|
attr_reader :n
|
|
15
26
|
attr_reader :w
|
|
16
27
|
|
|
17
28
|
# @param [Hash] opts
|
|
29
|
+
# @option opts [String] :name the name of the parameter set; it is a member of +SUPPORTED_NAMES+.
|
|
18
30
|
# @option opts [Integer] :n the message length as well as the length of a private key,
|
|
19
31
|
# public key, or signature element in bytes.
|
|
20
32
|
# @option opts [Integer] :w the Winternitz parameter; it is a member of the set {4, 16}.
|
|
21
|
-
# @
|
|
33
|
+
# @raise ArgumentError
|
|
22
34
|
def initialize(opts)
|
|
23
35
|
raise ArgumentError, 'name must be string.' unless opts[:name].is_a?(String)
|
|
24
36
|
raise ArgumentError, 'n must be integer.' unless opts[:n].is_a?(Integer)
|
|
25
37
|
raise ArgumentError, 'w must be integer.' unless opts[:w].is_a?(Integer)
|
|
26
38
|
|
|
39
|
+
expected_n = SUPPORTED_NAMES[opts[:name]]
|
|
40
|
+
raise ArgumentError, "name must be one of #{SUPPORTED_NAMES.keys.join(', ')}." if expected_n.nil?
|
|
41
|
+
raise ArgumentError, "n must be #{expected_n} for #{opts[:name]}." unless opts[:n] == expected_n
|
|
42
|
+
raise ArgumentError, "w must be one of #{SUPPORTED_W.join(', ')}." unless SUPPORTED_W.include?(opts[:w])
|
|
43
|
+
|
|
27
44
|
@name = opts[:name]
|
|
28
45
|
@n = opts[:n]
|
|
29
46
|
@w = opts[:w]
|
|
30
47
|
end
|
|
31
48
|
|
|
49
|
+
# log2(w), i.e. the number of bits consumed by a single base w digit.
|
|
50
|
+
# @return [Integer]
|
|
51
|
+
def lg_w
|
|
52
|
+
@lg_w ||= Math.log2(w).to_i
|
|
53
|
+
end
|
|
54
|
+
|
|
32
55
|
def len1
|
|
33
56
|
@len1 ||= (8.0 * n / Math.log2(w)).ceil
|
|
34
57
|
end
|
|
@@ -41,42 +64,55 @@ module WOTS
|
|
|
41
64
|
@len ||= len1 + len2
|
|
42
65
|
end
|
|
43
66
|
|
|
67
|
+
# @param [String] k Hex string.
|
|
68
|
+
# @param [String] m Hex string.
|
|
69
|
+
# @return [String] Hex string.
|
|
44
70
|
def f(k, m)
|
|
45
71
|
keyed_hash(0, k, m)
|
|
46
72
|
end
|
|
47
73
|
|
|
74
|
+
# @param [String] k Hex string.
|
|
75
|
+
# @param [String] m Hex string.
|
|
76
|
+
# @return [String] Hex string.
|
|
48
77
|
def h(k, m)
|
|
49
78
|
keyed_hash(1, k, m)
|
|
50
79
|
end
|
|
51
80
|
|
|
81
|
+
# @param [String] k Hex string.
|
|
82
|
+
# @param [String] m Hex string.
|
|
83
|
+
# @return [String] Hex string.
|
|
52
84
|
def h_msg(k, m)
|
|
53
85
|
keyed_hash(2, k, m)
|
|
54
86
|
end
|
|
55
87
|
|
|
56
88
|
# PRF function.
|
|
57
|
-
# @param [String] k key
|
|
58
|
-
# @param [String] m message
|
|
89
|
+
# @param [String] k key Hex string.
|
|
90
|
+
# @param [String] m message Hex string.
|
|
59
91
|
# @return [String] Hex string.
|
|
60
92
|
def prf(k, m)
|
|
61
93
|
keyed_hash(3, k, m)
|
|
62
94
|
end
|
|
63
95
|
|
|
64
96
|
# Convert data as base w representation.
|
|
65
|
-
# @param [String] data The data to be converted.
|
|
97
|
+
# @param [String] data The data to be converted. Hex string.
|
|
66
98
|
# @param [Integer] out_len Output length.
|
|
67
99
|
# @return [Array] An array of integer.
|
|
100
|
+
# @raise ArgumentError If +data+ is too short to produce +out_len+ digits.
|
|
68
101
|
def base_w(data, out_len)
|
|
69
102
|
x = hex_to_bin(data)
|
|
103
|
+
raise ArgumentError, 'out_len must be integer.' unless out_len.is_a?(Integer)
|
|
104
|
+
raise ArgumentError, 'out_len must be positive.' unless out_len.positive?
|
|
105
|
+
|
|
106
|
+
required = ((out_len * lg_w) / 8.0).ceil
|
|
107
|
+
raise ArgumentError, "data must be at least #{required} bytes to produce #{out_len} digits." if x.bytesize < required
|
|
70
108
|
|
|
71
109
|
basew = []
|
|
72
110
|
in_idx = 0
|
|
73
111
|
total = 0
|
|
74
112
|
bits = 0
|
|
75
|
-
lg_w = Math.log2(w).to_i
|
|
76
113
|
|
|
77
114
|
out_len.times do
|
|
78
115
|
if bits == 0
|
|
79
|
-
break if in_idx >= x.bytesize
|
|
80
116
|
total = x.getbyte(in_idx)
|
|
81
117
|
in_idx += 1
|
|
82
118
|
bits += 8
|
|
@@ -91,28 +127,35 @@ module WOTS
|
|
|
91
127
|
|
|
92
128
|
# Compute checksum for +base_w+.
|
|
93
129
|
# @param [Array] base_w
|
|
94
|
-
# @return [String] Checksum
|
|
130
|
+
# @return [String] Checksum hex string.
|
|
95
131
|
def compute_checksum(base_w)
|
|
96
132
|
c_sum = 0
|
|
97
133
|
len1.times do |i|
|
|
98
134
|
c_sum = (c_sum + w - 1 - base_w[i])
|
|
99
135
|
end
|
|
100
|
-
c_sum = (c_sum << (8 - ((len2 *
|
|
101
|
-
len_2_bytes = ((len2 *
|
|
102
|
-
to_byte(c_sum, len_2_bytes)
|
|
136
|
+
c_sum = (c_sum << (8 - ((len2 * lg_w) % 8)))
|
|
137
|
+
len_2_bytes = ((len2 * lg_w) / 8.0).ceil
|
|
138
|
+
bin_to_hex(to_byte(c_sum, len_2_bytes))
|
|
103
139
|
end
|
|
104
140
|
|
|
105
141
|
# WOTS+ Chaining Function.
|
|
106
142
|
# @see https://datatracker.ietf.org/doc/html/rfc8391#autoid-16
|
|
107
|
-
# @param [String] x Input string.
|
|
143
|
+
# @param [String] x Input string. Hex string.
|
|
108
144
|
# @param [Integer] start_idx Start index.
|
|
109
145
|
# @param [Integer] steps Number of steps.
|
|
110
|
-
# @param [String] seed Seed.
|
|
146
|
+
# @param [String] seed Seed. Hex string.
|
|
111
147
|
# @param [WOTS::Address] addr Address.
|
|
112
|
-
# @return [String]
|
|
148
|
+
# @return [String] Hex string.
|
|
149
|
+
# @raise ArgumentError
|
|
113
150
|
def chain(x, start_idx, steps, seed, addr)
|
|
151
|
+
raise ArgumentError, 'x must be hex string.' unless hex_string?(x)
|
|
152
|
+
raise ArgumentError, 'start_idx must be integer.' unless start_idx.is_a?(Integer)
|
|
153
|
+
raise ArgumentError, 'steps must be integer.' unless steps.is_a?(Integer)
|
|
154
|
+
raise ArgumentError, 'start_idx must not be negative.' if start_idx.negative?
|
|
155
|
+
raise ArgumentError, 'steps must not be negative.' if steps.negative?
|
|
156
|
+
raise ArgumentError, 'Invalid range' if (start_idx + steps) > (w - 1)
|
|
157
|
+
|
|
114
158
|
return x if steps == 0
|
|
115
|
-
raise "Invalid range" if (start_idx + steps) > (w - 1)
|
|
116
159
|
|
|
117
160
|
result = x.dup
|
|
118
161
|
|
|
@@ -120,10 +163,10 @@ module WOTS
|
|
|
120
163
|
addr.hash_addr = (start_idx + i)
|
|
121
164
|
|
|
122
165
|
addr.key_and_mask = 0
|
|
123
|
-
key = prf(seed, addr.to_payload)
|
|
166
|
+
key = prf(seed, bin_to_hex(addr.to_payload))
|
|
124
167
|
|
|
125
168
|
addr.key_and_mask = 1
|
|
126
|
-
mask = prf(seed, addr.to_payload)
|
|
169
|
+
mask = prf(seed, bin_to_hex(addr.to_payload))
|
|
127
170
|
|
|
128
171
|
masked = xor_bytes(result, mask)
|
|
129
172
|
result = f(key, masked)
|
|
@@ -140,8 +183,14 @@ module WOTS
|
|
|
140
183
|
# Convert +value+ to +length+ size binary string.
|
|
141
184
|
# @param [Integer] value
|
|
142
185
|
# @param [Integer] length
|
|
143
|
-
# @return [String]
|
|
186
|
+
# @return [String] Binary string.
|
|
187
|
+
# @raise ArgumentError If +value+ does not fit in +length+ bytes.
|
|
144
188
|
def to_byte(value, length)
|
|
189
|
+
raise ArgumentError, 'value must be integer.' unless value.is_a?(Integer)
|
|
190
|
+
raise ArgumentError, 'length must be integer.' unless length.is_a?(Integer)
|
|
191
|
+
raise ArgumentError, 'value must not be negative.' if value.negative?
|
|
192
|
+
raise ArgumentError, "value does not fit in #{length} bytes." if value.bit_length > length * 8
|
|
193
|
+
|
|
145
194
|
bytes = []
|
|
146
195
|
|
|
147
196
|
length.times do
|
|
@@ -154,12 +203,22 @@ module WOTS
|
|
|
154
203
|
|
|
155
204
|
private
|
|
156
205
|
|
|
206
|
+
# XOR two hex strings of the same length.
|
|
207
|
+
# @param [String] a Hex string.
|
|
208
|
+
# @param [String] b Hex string.
|
|
209
|
+
# @return [String] Hex string.
|
|
157
210
|
def xor_bytes(a, b)
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
211
|
+
x = hex_to_bin(a)
|
|
212
|
+
y = hex_to_bin(b)
|
|
213
|
+
raise ArgumentError, 'length mismatch.' unless x.bytesize == y.bytesize
|
|
214
|
+
|
|
215
|
+
bin_to_hex(x.unpack('C*').zip(y.unpack('C*')).map { |i, j| i ^ j }.pack("C*"))
|
|
161
216
|
end
|
|
162
217
|
|
|
218
|
+
# @param [Integer] prefix Domain separation prefix.
|
|
219
|
+
# @param [String] k Hex string.
|
|
220
|
+
# @param [String] m Hex string.
|
|
221
|
+
# @return [String] Hex string.
|
|
163
222
|
def keyed_hash(prefix, k, m)
|
|
164
223
|
payload = to_byte(prefix, n) + hex_to_bin(k) + hex_to_bin(m)
|
|
165
224
|
case name
|
|
@@ -174,4 +233,4 @@ module WOTS
|
|
|
174
233
|
end
|
|
175
234
|
end
|
|
176
235
|
end
|
|
177
|
-
end
|
|
236
|
+
end
|
data/lib/wots/private_key.rb
CHANGED
|
@@ -22,30 +22,33 @@ module WOTS
|
|
|
22
22
|
|
|
23
23
|
# Generate private key using +seed+.
|
|
24
24
|
# @param [WOTS::Param] param
|
|
25
|
-
# @param [String] seed
|
|
25
|
+
# @param [String] seed The secret seed. Hex string of +param.n+ bytes.
|
|
26
|
+
# It must be generated with a cryptographically secure random number generator.
|
|
26
27
|
# @raise ArgumentError
|
|
27
28
|
def self.from_seed(param, seed)
|
|
28
29
|
raise ArgumentError, "param must be WOTS::Param." unless param.is_a?(WOTS::Param)
|
|
29
|
-
raise ArgumentError, "seed must be
|
|
30
|
+
raise ArgumentError, "seed must be hex string." unless hex_string?(seed)
|
|
30
31
|
raise ArgumentError, "seed must be #{param.n} bytes." unless hex_to_bin(seed).bytesize == param.n
|
|
31
32
|
raise ArgumentError, "len parameter too large." if param.len.bit_length > 16
|
|
32
33
|
|
|
33
34
|
keys = param.len.times.map do |i|
|
|
34
|
-
param.prf(seed, param.to_byte(i, 32))
|
|
35
|
+
param.prf(seed, bin_to_hex(param.to_byte(i, 32)))
|
|
35
36
|
end
|
|
36
37
|
|
|
37
38
|
PrivateKey.new(param, keys)
|
|
38
39
|
end
|
|
39
40
|
|
|
40
41
|
# Generate signature.
|
|
41
|
-
#
|
|
42
|
-
#
|
|
42
|
+
# WOTS+ is a one-time signature scheme: this private key must never be used
|
|
43
|
+
# to sign more than one message.
|
|
44
|
+
# @param [String] pub_seed The Public seed. Hex string of +param.n+ bytes.
|
|
45
|
+
# @param [String] message The message to be signed. Hex string of +param.n+ bytes.
|
|
43
46
|
# @return [WOTS::Signature]
|
|
44
47
|
# @raise ArgumentError
|
|
45
48
|
def sign(pub_seed, message)
|
|
46
49
|
raise ArgumentError, 'pub_seed must be hex string.' unless hex_string?(pub_seed)
|
|
47
50
|
raise ArgumentError, "pub_seed must be #{param.n} bytes." unless hex_to_bin(pub_seed).bytesize == param.n
|
|
48
|
-
raise ArgumentError, "message must be string." unless
|
|
51
|
+
raise ArgumentError, "message must be hex string." unless hex_string?(message)
|
|
49
52
|
raise ArgumentError, "message must be #{param.n} bytes." unless hex_to_bin(message).bytesize == param.n
|
|
50
53
|
|
|
51
54
|
addr = Address.new
|
|
@@ -65,5 +68,13 @@ module WOTS
|
|
|
65
68
|
|
|
66
69
|
Signature.new(param, sigs)
|
|
67
70
|
end
|
|
71
|
+
|
|
72
|
+
# Redacted inspect.
|
|
73
|
+
# The default implementation prints every instance variable, which would
|
|
74
|
+
# write the whole private key into logs, exception reports and REPL output.
|
|
75
|
+
# @return [String]
|
|
76
|
+
def inspect
|
|
77
|
+
"#<WOTS::PrivateKey param=#{param.name} w=#{param.w} keys=[REDACTED]>"
|
|
78
|
+
end
|
|
68
79
|
end
|
|
69
80
|
end
|
data/lib/wots/public_key.rb
CHANGED
|
@@ -21,7 +21,8 @@ module WOTS
|
|
|
21
21
|
|
|
22
22
|
# Generate public key from +private_key+ and +pub_seed+.
|
|
23
23
|
# @param [WOTS::PrivateKey] private_key
|
|
24
|
-
# @param [String] pub_seed
|
|
24
|
+
# @param [String] pub_seed Hex string of +param.n+ bytes.
|
|
25
|
+
# @raise ArgumentError
|
|
25
26
|
def self.from_private_key(private_key, pub_seed)
|
|
26
27
|
raise ArgumentError, 'private_key must be WOTS::PrivateKey.' unless private_key.is_a?(WOTS::PrivateKey)
|
|
27
28
|
param = private_key.param
|
|
@@ -38,15 +39,16 @@ module WOTS
|
|
|
38
39
|
|
|
39
40
|
# Generate public key from +signature+.
|
|
40
41
|
# @param [WOTS::Signature] signature
|
|
41
|
-
# @param [String] pub_seed
|
|
42
|
-
# @param [String] message
|
|
42
|
+
# @param [String] pub_seed Hex string of +param.n+ bytes.
|
|
43
|
+
# @param [String] message Hex string of +param.n+ bytes.
|
|
43
44
|
# @return [WOTS::PublicKey]
|
|
44
45
|
# @raise ArgumentError
|
|
45
46
|
def self.from_signature(signature, pub_seed, message)
|
|
47
|
+
raise ArgumentError, 'signature must be WOTS::Signature.' unless signature.is_a?(WOTS::Signature)
|
|
46
48
|
param = signature.param
|
|
47
49
|
raise ArgumentError, 'pub_seed must be hex string.' unless hex_string?(pub_seed)
|
|
48
50
|
raise ArgumentError, "pub_seed must be #{param.n} bytes." unless hex_to_bin(pub_seed).bytesize == param.n
|
|
49
|
-
raise ArgumentError, "message must be string." unless
|
|
51
|
+
raise ArgumentError, "message must be hex string." unless hex_string?(message)
|
|
50
52
|
raise ArgumentError, "message must be #{param.n} bytes." unless hex_to_bin(message).bytesize == param.n
|
|
51
53
|
|
|
52
54
|
base_w = param.base_w(message, param.len1)
|
data/lib/wots/util.rb
CHANGED
|
@@ -2,31 +2,33 @@ module WOTS
|
|
|
2
2
|
module Util
|
|
3
3
|
|
|
4
4
|
# Check whether +data+ is hex string or not.
|
|
5
|
+
# An odd-length string is NOT a valid hex string, since +Array#pack+ would
|
|
6
|
+
# silently zero-pad it and make two distinct inputs collide.
|
|
5
7
|
# @param [String] data
|
|
6
8
|
# @return [Boolean]
|
|
7
9
|
# @raise [ArgumentError]
|
|
8
10
|
def hex_string?(data)
|
|
9
11
|
raise ArgumentError, 'data must be string' unless data.is_a?(String)
|
|
10
|
-
data.match?(/\A[0-9a-fA-F]+\z/)
|
|
12
|
+
data.length.even? && data.match?(/\A[0-9a-fA-F]+\z/)
|
|
11
13
|
end
|
|
12
14
|
|
|
13
15
|
# Convert hex string +data+ to binary.
|
|
14
|
-
# @param [String] data
|
|
15
|
-
# @return [String]
|
|
16
|
-
# @raise [ArgumentError]
|
|
16
|
+
# @param [String] data Hex string.
|
|
17
|
+
# @return [String] Binary string.
|
|
18
|
+
# @raise [ArgumentError] If +data+ is not a hex string.
|
|
17
19
|
def hex_to_bin(data)
|
|
18
|
-
raise ArgumentError, 'data must be string' unless
|
|
19
|
-
|
|
20
|
+
raise ArgumentError, 'data must be hex string' unless hex_string?(data)
|
|
21
|
+
[data].pack('H*')
|
|
20
22
|
end
|
|
21
23
|
|
|
22
24
|
# Convert binary string +data+ to hex string.
|
|
23
|
-
# @param [String] data
|
|
24
|
-
# @return [String]
|
|
25
|
+
# @param [String] data Binary string.
|
|
26
|
+
# @return [String] Hex string.
|
|
25
27
|
# @raise [ArgumentError]
|
|
26
28
|
def bin_to_hex(data)
|
|
27
29
|
raise ArgumentError, 'data must be string' unless data.is_a?(String)
|
|
28
|
-
|
|
30
|
+
data.unpack1('H*')
|
|
29
31
|
end
|
|
30
32
|
|
|
31
33
|
end
|
|
32
|
-
end
|
|
34
|
+
end
|
data/lib/wots/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: wots
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- azuchi
|
|
@@ -57,7 +57,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
57
57
|
- !ruby/object:Gem::Version
|
|
58
58
|
version: '0'
|
|
59
59
|
requirements: []
|
|
60
|
-
rubygems_version:
|
|
60
|
+
rubygems_version: 4.0.3
|
|
61
61
|
specification_version: 4
|
|
62
62
|
summary: A Ruby implementation of the Winternitz OTS (W-OTS+), as described in RFC8391.
|
|
63
63
|
test_files: []
|