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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d6efca98c49499aae50fd5225762614026111f16cfc4ccee573e6ea6857e0664
4
- data.tar.gz: 236faef3ed700463ea083a7cae7de8d6365663e5ad1e65c9819dab21e92fb6d3
3
+ metadata.gz: af0b30e037fbb6e5727fa6f90a7c601625d9a959268b28e7d48bb92ab5db1b37
4
+ data.tar.gz: 5af9c89ca3b088bef40f3f6c1307a4c8fc9c108d54967ad5670118b0e87730bc
5
5
  SHA512:
6
- metadata.gz: b8eefc4c86bf05cfa7820e291bcbacf3d1fba792d91a7392f086147979468e8fe8caa62de59c7f47c00bc65145ab9d82f91cbba35215f295ddb85484dea6d477
7
- data.tar.gz: 0a3a1affaa6a0d9e9298927cfd16e12a3e84aaa4a8b558f4b739002e2005a4d07f0747156c2716c12e879d5770b3c7aa06a852a47f148b9b3ebeb32f7026d27a
6
+ metadata.gz: f4171a56cc4b0245e5e5b2f73951a8a5f2d743d488161bc5d7775370a77e1ba0e992d928a933cb30cd7657ed494d65c8864ae17ba79ac1cad4fc0838b70b9ec3
7
+ data.tar.gz: '009254475b5624eabe23d3f7d96a050323c36d0894c911f078a7cb7c3c4dd1cae000fb3683637441b8d2b242ff6e2678b1adb9e6c08a301995712ea0eee38dd8'
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- ruby-3.4.3
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 cryptograFixphic library implementation. While it follows RFC 8391 specifications,
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
- attr_accessor :layer_addr
4
- attr_accessor :tree_addr
5
- attr_accessor :ots_addr
6
- attr_accessor :chain_addr
7
- attr_accessor :hash_addr
8
- attr_accessor :key_and_mask # 0: key generation, 1: bitmask generation
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
- @layer_addr = layer_addr
12
- @tree_addr = tree_addr
13
- @ots_addr = ots_addr
14
- @chain_addr = chain_addr
15
- @hash_addr = hash_addr
16
- @key_and_mask = key_and_mask
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
- # @option opts [Integer] :len the number of n-byte string elements in a WOTS+ private key, public key, and signature.
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 binary string.
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 * Math.log2(w).to_i) % 8)))
101
- len_2_bytes = ((len2 * Math.log2(w).to_i) / 8.0).ceil
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] Result.
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
- [a].pack('H*').unpack('C*').zip(
159
- [b].pack('H*').unpack('C*')
160
- ).map { |x, y| x ^ y }.pack("C*")
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
@@ -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 String." unless seed.is_a?(String)
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
- # @param [String] pub_seed The Public seed.
42
- # @param [String] message The message to be signed.
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 message.is_a?(String)
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
@@ -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 message.is_a?(String)
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 data.is_a?(String)
19
- hex_string?(data) ? [data].pack('H*') : data
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
- hex_string?(data) ? data : data.unpack1('H*')
30
+ data.unpack1('H*')
29
31
  end
30
32
 
31
33
  end
32
- end
34
+ end
data/lib/wots/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module WOTS
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
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.1.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: 3.6.7
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: []