wots 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/CHANGELOG.md +70 -0
- data/README.md +109 -10
- data/lib/wots/address.rb +11 -0
- data/lib/wots/core.rb +189 -0
- data/lib/wots/param.rb +98 -96
- data/lib/wots/private_key.rb +24 -23
- data/lib/wots/public_key.rb +32 -19
- data/lib/wots/rfc8391_hasher.rb +105 -0
- data/lib/wots/signature.rb +11 -1
- data/lib/wots/util.rb +13 -1
- data/lib/wots/version.rb +1 -1
- data/lib/wots.rb +2 -0
- metadata +22 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 44928dd35ba7e5a8294069e3f85916e3a635440d52d63304337f218503d66443
|
|
4
|
+
data.tar.gz: 1e2b102c71bb6d73c96a5aa31cee0a74b4de756179b791a68733e3233e86c396
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c92615a1dece0f8ed08dcd809c32dd8f4c4bf2476accd34799a66f3dd277ecb6bedf3ce97c6f67132bd46a8ca09b7e542497b715f515b6d4180ab9f59426e386
|
|
7
|
+
data.tar.gz: 2a1f7a07e0860068fec8b1dc2c685efd02f8e287971d711244642979bb26a52233f338135696b86d77ef59b0eb00baf6bdb2099b23956d482d57677d0c509ce6
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
5
|
+
|
|
6
|
+
## [0.3.0] - 2026-11-08
|
|
7
|
+
|
|
8
|
+
This release contains **breaking changes**. Keys and signatures created with 0.2.0 are not compatible.
|
|
9
|
+
|
|
10
|
+
### Breaking changes
|
|
11
|
+
|
|
12
|
+
- `WOTSP-SHAKE_256` (`WOTS::Param::SHAKE256`) now uses **SHAKE128** with a 256-bit output, as defined in
|
|
13
|
+
RFC 8391 Sections 5.1 and 5.2. It used SHAKE256 with OpenSSL's default output length before, which is not
|
|
14
|
+
RFC 8391 and fails on OpenSSL 3.4 and later. Its outputs change.
|
|
15
|
+
- `PrivateKey.from_seed(param, seed, pub_seed, addr: Address.new)` now requires `pub_seed`, and derives each
|
|
16
|
+
private key element as `PRF_keygen(seed, pub_seed || ADRS)` (NIST SP 800-208 Section 6.2), the method of the
|
|
17
|
+
XMSS reference implementation. It used `PRF(seed, toByte(i, 32))` before, so the same seed always gave the
|
|
18
|
+
same key regardless of `pub_seed` and address. The derived keys change.
|
|
19
|
+
- `PrivateKey#keys`, `PublicKey#keys` and `Signature#sigs` are frozen arrays of frozen, lowercase hex strings.
|
|
20
|
+
- `WOTS::Param` instances are frozen.
|
|
21
|
+
- Stricter input validation, raising `ArgumentError`:
|
|
22
|
+
- `Param#f`, `#h`, `#h_msg`, `#prf` and `#prf_keygen` accept only the key and message lengths defined in
|
|
23
|
+
RFC 8391 and SP 800-208.
|
|
24
|
+
- `Param#chain` validates the seed length, and `Param#compute_checksum` rejects digits outside `[0, w - 1]`.
|
|
25
|
+
- Strings in encodings that are not ASCII compatible (e.g. UTF-16) are not accepted as hex strings.
|
|
26
|
+
- `Param#chain` no longer modifies the given address.
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- `addr:` keyword (default `WOTS::Address.new`) on `PrivateKey.from_seed`, `PrivateKey#sign`,
|
|
31
|
+
`PublicKey.from_private_key` and `PublicKey.from_signature`, so that one seed can derive several keys.
|
|
32
|
+
- `PublicKey#verify(signature, pub_seed, message, addr:)`.
|
|
33
|
+
- `Signature#==`, and `eql?`/`hash` consistent with `==` for `PublicKey` and `Signature`.
|
|
34
|
+
- `WOTS::Core`, a scheme-independent WOTS+ engine on binary strings, driven by a hasher object
|
|
35
|
+
(`secret`, `step`, `compress`), so that other WOTS+ instantiations such as the one of SLH-DSA can be built on it.
|
|
36
|
+
- `WOTS::RFC8391Hasher`, the hasher of RFC 8391.
|
|
37
|
+
- `Param#keyed_digest` (binary keyed hash) and the prefix constants `Param::F`, `H`, `H_MSG`, `PRF`, `PRF_KEYGEN`.
|
|
38
|
+
- Test vectors generated with the XMSS reference implementation for every parameter set, `w = 4` and `16`.
|
|
39
|
+
- Runtime dependency on the `sha3` gem.
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
|
|
43
|
+
- Signing, key generation and verification are about 30% faster.
|
|
44
|
+
- Public keys and signatures compare equal regardless of the letter case of their hex strings.
|
|
45
|
+
|
|
46
|
+
### Security
|
|
47
|
+
|
|
48
|
+
- Documented how to manage one-time keys: one address per key, persist the state before releasing a
|
|
49
|
+
signature, never sign twice, and never roll back or share the state. The library does not track key use.
|
|
50
|
+
- `RFC8391Hasher#inspect` redacts the seed and private key elements.
|
|
51
|
+
- Releases require MFA on RubyGems (`rubygems_mfa_required`).
|
|
52
|
+
- CI runs with a read-only token and actions pinned to commit SHAs.
|
|
53
|
+
|
|
54
|
+
## [0.2.0] - 2026-08-11
|
|
55
|
+
|
|
56
|
+
### Security
|
|
57
|
+
|
|
58
|
+
- Restricted parameters to the sets and Winternitz parameters (`w = 4, 16`) defined in RFC 8391.
|
|
59
|
+
- Rejected odd-length hex and raw binary input, and validated the remaining inputs.
|
|
60
|
+
- Validated `Address` field ranges.
|
|
61
|
+
- Redacted private key material from `PrivateKey#inspect`.
|
|
62
|
+
- Documented the input format and security constraints.
|
|
63
|
+
|
|
64
|
+
## [0.1.0] - 2025-09-18
|
|
65
|
+
|
|
66
|
+
- Initial release.
|
|
67
|
+
|
|
68
|
+
[0.3.0]: https://github.com/azuchi/wots/compare/v0.2.0...v0.3.0
|
|
69
|
+
[0.2.0]: https://github.com/azuchi/wots/compare/v0.1.0...v0.2.0
|
|
70
|
+
[0.1.0]: https://github.com/azuchi/wots/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -10,7 +10,7 @@ WOTS+ is a post-quantum cryptographic signature scheme that provides security ag
|
|
|
10
10
|
- Support for multiple hash functions:
|
|
11
11
|
- `WOTSP-SHA2_256` (SHA-256)
|
|
12
12
|
- `WOTSP-SHA2_512` (SHA-512)
|
|
13
|
-
- `WOTSP-SHAKE_256` (
|
|
13
|
+
- `WOTSP-SHAKE_256` (SHAKE128 with a 256-bit output, as defined in RFC 8391)
|
|
14
14
|
- Support for Winternitz parameters w=4 and w=16
|
|
15
15
|
- Key generation from seed
|
|
16
16
|
- Digital signature generation and verification
|
|
@@ -41,24 +41,42 @@ require 'wots'
|
|
|
41
41
|
# Use predefined parameters
|
|
42
42
|
param = WOTS::Param::SHA256
|
|
43
43
|
|
|
44
|
-
# Generate private key from seed
|
|
44
|
+
# Generate private key from the secret seed and the public seed
|
|
45
45
|
seed = "693141c7ee701d13e1a7c733e0aa8326c19961429bfb54083f2f65b30c32e20b"
|
|
46
|
-
|
|
46
|
+
pub_seed = "46ece585b4c0bfa1186209270e22fa07c4716461b5a026c268e594fb94404f3a"
|
|
47
|
+
private_key = WOTS::PrivateKey.from_seed(param, seed, pub_seed)
|
|
47
48
|
|
|
48
49
|
# Generate public key
|
|
49
|
-
pub_seed = "46ece585b4c0bfa1186209270e22fa07c4716461b5a026c268e594fb94404f3a"
|
|
50
50
|
public_key = WOTS::PublicKey.from_private_key(private_key, pub_seed)
|
|
51
51
|
|
|
52
52
|
# Sign a message
|
|
53
53
|
message = "f16c96e88fb99a8287a43121962e89ed521699fa3e126c67eaaa168066354477"
|
|
54
54
|
signature = private_key.sign(pub_seed, message)
|
|
55
55
|
|
|
56
|
-
# Verify signature
|
|
57
|
-
|
|
58
|
-
valid = (recovered_pk == public_key)
|
|
56
|
+
# Verify signature
|
|
57
|
+
valid = public_key.verify(signature, pub_seed, message)
|
|
59
58
|
puts "Signature valid: #{valid}"
|
|
60
59
|
```
|
|
61
60
|
|
|
61
|
+
### Deriving several keys from one seed
|
|
62
|
+
|
|
63
|
+
Private keys are derived as `sk[i] = PRF_keygen(seed, pub_seed || ADRS)`
|
|
64
|
+
([NIST SP 800-208](https://doi.org/10.6028/NIST.SP.800-208) Section 6.2), so each address gives a different key.
|
|
65
|
+
Pass a distinct `WOTS::Address` (e.g. a different `ots_addr`) for each key, and pass the **same** address to
|
|
66
|
+
every operation on that key:
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
addr = WOTS::Address.new(ots_addr: 5)
|
|
70
|
+
private_key = WOTS::PrivateKey.from_seed(param, seed, pub_seed, addr: addr)
|
|
71
|
+
public_key = WOTS::PublicKey.from_private_key(private_key, pub_seed, addr: addr)
|
|
72
|
+
signature = private_key.sign(pub_seed, message, addr: addr)
|
|
73
|
+
public_key.verify(signature, pub_seed, message, addr: addr) # => true
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`addr` defaults to `WOTS::Address.new` (all fields zero). Never use the same `seed`, `pub_seed` and address for
|
|
77
|
+
two keys: they would be the same key, and signing with both would reuse a one-time key.
|
|
78
|
+
See [One-time keys](#one-time-keys-state-management-is-up-to-you) for how to manage the addresses.
|
|
79
|
+
|
|
62
80
|
### Available Parameter Sets
|
|
63
81
|
|
|
64
82
|
```ruby
|
|
@@ -68,10 +86,40 @@ param = WOTS::Param::SHA256
|
|
|
68
86
|
# SHA-512 based (n=64 bytes, w=16)
|
|
69
87
|
param = WOTS::Param::SHA512
|
|
70
88
|
|
|
71
|
-
#
|
|
89
|
+
# SHAKE128 based (n=32 bytes, w=16), i.e. RFC 8391 WOTSP-SHAKE_256
|
|
72
90
|
param = WOTS::Param::SHAKE256
|
|
73
91
|
```
|
|
74
92
|
|
|
93
|
+
`WOTS::Param::SHAKE256` is named after the RFC 8391 parameter set `WOTSP-SHAKE_256`, where `256` is the output
|
|
94
|
+
length in bits. It uses **SHAKE128**, not SHAKE256 (RFC 8391 Section 5.1).
|
|
95
|
+
|
|
96
|
+
## Using the WOTS+ engine from another library
|
|
97
|
+
|
|
98
|
+
`WOTS::Core` is the scheme-independent WOTS+ engine behind the API above. It works on **binary strings**
|
|
99
|
+
and leaves the hashing and the hash address to a *hasher* object, so the same engine can drive other WOTS+
|
|
100
|
+
instantiations, such as the one in SLH-DSA (FIPS 205), implemented in another library.
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
core = WOTS::Core.new(n: 16, w: 16) # w is 4 or 16
|
|
104
|
+
pk = core.pk_gen(hasher)
|
|
105
|
+
sig = core.sign(hasher, msg) # msg: n-byte binary string; returns len n-byte strings
|
|
106
|
+
pk2 = core.pk_from_sig(hasher, sig, msg)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
A hasher is bound to one WOTS+ key pair and responds to:
|
|
110
|
+
|
|
111
|
+
| Method | Returns |
|
|
112
|
+
|---|---|
|
|
113
|
+
| `secret(chain_idx)` | the `n`-byte secret value of chain `chain_idx` (used by `pk_gen` and `sign` only) |
|
|
114
|
+
| `step(x, chain_idx, hash_idx)` | one application of the chaining hash to the `n`-byte value `x` at position `hash_idx` of chain `chain_idx` |
|
|
115
|
+
| `compress(values)` | the public key built from the `len` chain end values |
|
|
116
|
+
|
|
117
|
+
`WOTS::Core` calls them in the order of FIPS 205 Algorithms 6–8: for each chain `i` in ascending order,
|
|
118
|
+
`secret(i)` (when needed) and then `step(x, i, j)` for ascending `j`. It never sees an address, so the hasher
|
|
119
|
+
sets the chain and hash address itself. `WOTS::RFC8391Hasher` is the hasher of RFC 8391 used by this library.
|
|
120
|
+
|
|
121
|
+
A hasher keeps a mutable address, so do not share one hasher between threads; create one per operation.
|
|
122
|
+
|
|
75
123
|
## Input format
|
|
76
124
|
|
|
77
125
|
All key material, seeds and messages are **even-length hexadecimal strings**. Raw binary strings and
|
|
@@ -87,11 +135,39 @@ signature = private_key.sign(pub_seed, message)
|
|
|
87
135
|
**⚠️ IMPORTANT: This is a cryptographic library implementation. While it follows RFC 8391 specifications,
|
|
88
136
|
it has not undergone formal security audits. Use at your own risk in production environments.**
|
|
89
137
|
|
|
90
|
-
|
|
91
|
-
|
|
138
|
+
### One-time keys: state management is up to you
|
|
139
|
+
|
|
140
|
+
WOTS+ is a **one-time signature scheme**. Each private key must be used to sign **one message, once**.
|
|
141
|
+
Signatures of two different messages under the same key let an attacker forge signatures.
|
|
142
|
+
|
|
143
|
+
**This library does not track whether a key has been used.** A flag inside `WOTS::PrivateKey` would not protect
|
|
144
|
+
you: calling `PrivateKey.from_seed` again recreates the same key as a fresh object, and real reuse happens across
|
|
145
|
+
process restarts, devices and backups, which no in-memory flag can see. The state belongs to the code that assigns
|
|
146
|
+
one-time keys, typically a counter of the next address to use. Follow these rules
|
|
147
|
+
(based on [NIST SP 800-208](https://doi.org/10.6028/NIST.SP.800-208) Sections 8.1 and 9.1):
|
|
148
|
+
|
|
149
|
+
- **One address per key.** `seed`, `pub_seed` and `addr` determine the key: the same triple always gives the same
|
|
150
|
+
key. Give every key its own address, e.g. by incrementing `ots_addr`.
|
|
151
|
+
- **Persist before you release.** Write the updated state (e.g. the next `ots_addr`) to non-volatile storage, and
|
|
152
|
+
make sure it has really reached it (e.g. `File#fsync`, a committed database transaction), *before* the signature
|
|
153
|
+
leaves your process. If the process crashes after exporting a signature but before the state is durable, the key
|
|
154
|
+
will be used again after the restart.
|
|
155
|
+
- **Never sign twice, not even the same message.** Re-computing a signature over the same message is open to fault
|
|
156
|
+
injection attacks (SP 800-208, Section 8.1, footnote 9). Sign once and store the signature if you need it again.
|
|
157
|
+
- **Never roll the state back or share it.** Do not restore the state from backups or VM snapshots, and do not use
|
|
158
|
+
the same `seed` on several devices or processes at once.
|
|
159
|
+
|
|
160
|
+
SP 800-208 additionally requires stateful hash-based key generation and signing to run inside a validated hardware
|
|
161
|
+
cryptographic module. This software library cannot meet that requirement.
|
|
162
|
+
|
|
163
|
+
### Key material
|
|
92
164
|
|
|
93
165
|
The seed passed to `WOTS::PrivateKey.from_seed` is secret key material and must come from a
|
|
94
166
|
cryptographically secure random number generator, e.g. `SecureRandom.hex(param.n)`.
|
|
167
|
+
The private key is bound to `pub_seed` and the address, using the key generation method of NIST SP 800-208
|
|
168
|
+
(Section 6.2). Of the parameter sets supported here, SP 800-208 defines
|
|
169
|
+
`PRF_keygen` only for `WOTSP-SHA2_256`; this library applies the same construction
|
|
170
|
+
(`Hash(toByte(4, n) || KEY || M)`) to every parameter set, as the XMSS reference implementation does.
|
|
95
171
|
|
|
96
172
|
Only the Winternitz parameters defined in RFC 8391 (`w = 4` and `w = 16`) are accepted. Other values
|
|
97
173
|
overflow the checksum encoding, which drops the high bits of the checksum and allows an attacker to
|
|
@@ -103,3 +179,26 @@ garbage collected.
|
|
|
103
179
|
## Specifications
|
|
104
180
|
|
|
105
181
|
This implementation follows [RFC 8391 - XMSS: eXtended Merkle Signature Scheme](https://datatracker.ietf.org/doc/html/rfc8391), Section 3 (WOTS+).
|
|
182
|
+
Private keys are derived with the method of [NIST SP 800-208](https://doi.org/10.6028/NIST.SP.800-208) Section 6.2.
|
|
183
|
+
|
|
184
|
+
### How it is tested
|
|
185
|
+
|
|
186
|
+
RFC 8391 and SP 800-208 do not publish test vectors; RFC 8391 (Section 7) points to its reference implementation
|
|
187
|
+
instead, and NIST ACVP has no XMSS or WOTS+ test vectors yet. `spec/fixtures/reference_vectors.json` was therefore
|
|
188
|
+
generated with the WOTS+ part (`wots.c`) of the
|
|
189
|
+
[XMSS reference implementation](https://github.com/XMSS/xmss-reference)
|
|
190
|
+
(commit `171ccbd26f098542a67eb5d2b128281c80bd71a6`) for every parameter set, `w = 4` and `16`, and a non-zero
|
|
191
|
+
address. The specs check that key generation, signing and verification reproduce those vectors.
|
|
192
|
+
|
|
193
|
+
Note that this is the reference implementation of XMSS, not of a standalone WOTS+. The chaining, signing and
|
|
194
|
+
verification are RFC 8391 WOTS+ itself. The private key derivation is the one XMSS uses (SP 800-208); RFC 8391
|
|
195
|
+
leaves the derivation of WOTS+ private keys open (Section 3.1.7).
|
|
196
|
+
|
|
197
|
+
### Not compatible with SLH-DSA
|
|
198
|
+
|
|
199
|
+
The WOTS+ of SLH-DSA ([FIPS 205](https://doi.org/10.6028/NIST.FIPS.205)) is a different instantiation: its chain
|
|
200
|
+
step is the tweakable hash `F(PK.seed, ADRS, M)` without bitmasks, its address layout differs (and is compressed
|
|
201
|
+
for SHA2), the public key is compressed with `T_len`, and it uses `n = 16, 24, 32` with `w = 16` only.
|
|
202
|
+
Keys and signatures of this library cannot be used with SLH-DSA, nor with schemes built on it. Such a WOTS+ can be
|
|
203
|
+
implemented on top of `WOTS::Core` with its own hasher (see
|
|
204
|
+
[Using the WOTS+ engine from another library](#using-the-wots-engine-from-another-library)).
|
data/lib/wots/address.rb
CHANGED
|
@@ -51,6 +51,17 @@ module WOTS
|
|
|
51
51
|
0
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
+
# Return a copy of +addr+ that can be freely mutated by the chaining function,
|
|
55
|
+
# so that the caller's address object is never modified.
|
|
56
|
+
# @param [WOTS::Address] addr
|
|
57
|
+
# @return [WOTS::Address]
|
|
58
|
+
# @raise ArgumentError If +addr+ is not a WOTS::Address.
|
|
59
|
+
def self.copy_of(addr)
|
|
60
|
+
raise ArgumentError, 'addr must be WOTS::Address.' unless addr.is_a?(Address)
|
|
61
|
+
|
|
62
|
+
addr.dup
|
|
63
|
+
end
|
|
64
|
+
|
|
54
65
|
def to_payload
|
|
55
66
|
[layer_addr, tree_addr, type, ots_addr, chain_addr, hash_addr, key_and_mask].pack('NQ>NNNNN')
|
|
56
67
|
end
|
data/lib/wots/core.rb
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
module WOTS
|
|
2
|
+
# Scheme independent WOTS+ engine working on binary strings.
|
|
3
|
+
#
|
|
4
|
+
# The WOTS+ of RFC 8391 and of FIPS 205 (SLH-DSA) share the message encoding,
|
|
5
|
+
# the checksum and the way the chains are walked; they differ only in how a
|
|
6
|
+
# chain secret is derived, how one chain step is hashed, what goes into the
|
|
7
|
+
# hash address and how the chain end values form the public key. Those parts
|
|
8
|
+
# are supplied by a +hasher+ object, so this class never sees an address.
|
|
9
|
+
#
|
|
10
|
+
# A hasher bound to one WOTS+ key pair responds to:
|
|
11
|
+
#
|
|
12
|
+
# * +secret(chain_idx)+ -> the +n+-byte secret value of chain +chain_idx+.
|
|
13
|
+
# Only needed by #pk_gen and #sign.
|
|
14
|
+
# * +step(x, chain_idx, hash_idx)+ -> one application of the chaining hash to the
|
|
15
|
+
# +n+-byte value +x+ at position +hash_idx+ of chain +chain_idx+
|
|
16
|
+
# (RFC 8391 Algorithm 2 with hash address +hash_idx+, FIPS 205 Algorithm 5 line 3-4).
|
|
17
|
+
# * +compress(values)+ -> the public key built from the +len+ chain end values.
|
|
18
|
+
# RFC 8391 returns the values as they are; FIPS 205 applies T_len.
|
|
19
|
+
#
|
|
20
|
+
# The calls are made in the order of FIPS 205 Algorithms 6-8: for each chain i
|
|
21
|
+
# (ascending), +secret(i)+ (if needed) and then +step(x, i, j)+ for ascending j.
|
|
22
|
+
#
|
|
23
|
+
# This class is the API other gems build on, so its interface is kept stable.
|
|
24
|
+
class Core
|
|
25
|
+
# The Winternitz parameters this engine supports.
|
|
26
|
+
SUPPORTED_W = [4, 16].freeze
|
|
27
|
+
|
|
28
|
+
attr_reader :n, :w, :lg_w, :len1, :len2, :len
|
|
29
|
+
|
|
30
|
+
# @param [Integer] n The byte length of messages, chain values and secrets.
|
|
31
|
+
# @param [Integer] w The Winternitz parameter, one of +SUPPORTED_W+.
|
|
32
|
+
# @raise ArgumentError
|
|
33
|
+
def initialize(n:, w:)
|
|
34
|
+
raise ArgumentError, 'n must be integer.' unless n.is_a?(Integer)
|
|
35
|
+
raise ArgumentError, 'n must be positive.' unless n.positive?
|
|
36
|
+
raise ArgumentError, 'w must be integer.' unless w.is_a?(Integer)
|
|
37
|
+
raise ArgumentError, "w must be one of #{SUPPORTED_W.join(', ')}." unless SUPPORTED_W.include?(w)
|
|
38
|
+
|
|
39
|
+
@n = n
|
|
40
|
+
@w = w
|
|
41
|
+
@lg_w = w.bit_length - 1
|
|
42
|
+
@len1 = (8 * n + lg_w - 1) / lg_w
|
|
43
|
+
# floor(log2(len1 * (w - 1)) / lg_w) + 1, computed without floating point.
|
|
44
|
+
@len2 = ((len1 * (w - 1)).bit_length - 1) / lg_w + 1
|
|
45
|
+
@len = len1 + len2
|
|
46
|
+
@csum_shift = (8 - ((len2 * lg_w) % 8)) % 8
|
|
47
|
+
@csum_bytes = (len2 * lg_w + 7) / 8
|
|
48
|
+
freeze
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Convert +data+ into +out_len+ base w digits (RFC 8391 base_w, FIPS 205 base_2b).
|
|
52
|
+
# @param [String] data Binary string.
|
|
53
|
+
# @param [Integer] out_len
|
|
54
|
+
# @return [Array<Integer>]
|
|
55
|
+
# @raise ArgumentError If +data+ is too short to produce +out_len+ digits.
|
|
56
|
+
def base_w(data, out_len)
|
|
57
|
+
raise ArgumentError, 'data must be string.' unless data.is_a?(String)
|
|
58
|
+
raise ArgumentError, 'out_len must be integer.' unless out_len.is_a?(Integer)
|
|
59
|
+
raise ArgumentError, 'out_len must be positive.' unless out_len.positive?
|
|
60
|
+
|
|
61
|
+
required = (out_len * lg_w + 7) / 8
|
|
62
|
+
raise ArgumentError, "data must be at least #{required} bytes to produce #{out_len} digits." if data.bytesize < required
|
|
63
|
+
|
|
64
|
+
digits = []
|
|
65
|
+
in_idx = 0
|
|
66
|
+
total = 0
|
|
67
|
+
bits = 0
|
|
68
|
+
mask = w - 1
|
|
69
|
+
out_len.times do
|
|
70
|
+
if bits == 0
|
|
71
|
+
total = data.getbyte(in_idx)
|
|
72
|
+
in_idx += 1
|
|
73
|
+
bits = 8
|
|
74
|
+
end
|
|
75
|
+
bits -= lg_w
|
|
76
|
+
digits << ((total >> bits) & mask)
|
|
77
|
+
end
|
|
78
|
+
digits
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Encode the checksum of the first +len1+ message digits.
|
|
82
|
+
# @param [Array<Integer>] msg_digits
|
|
83
|
+
# @return [String] Binary string of ceil(len2 * lg_w / 8) bytes.
|
|
84
|
+
# @raise ArgumentError
|
|
85
|
+
def checksum_bytes(msg_digits)
|
|
86
|
+
raise ArgumentError, 'msg_digits must be Array.' unless msg_digits.is_a?(Array)
|
|
87
|
+
raise ArgumentError, "msg_digits must have at least #{len1} digits." if msg_digits.length < len1
|
|
88
|
+
|
|
89
|
+
csum = 0
|
|
90
|
+
len1.times do |i|
|
|
91
|
+
d = msg_digits[i]
|
|
92
|
+
raise ArgumentError, "digit must be between 0 and #{w - 1}." unless d.is_a?(Integer) && d >= 0 && d < w
|
|
93
|
+
|
|
94
|
+
csum += w - 1 - d
|
|
95
|
+
end
|
|
96
|
+
csum <<= @csum_shift
|
|
97
|
+
bytes = Array.new(@csum_bytes) { |i| (csum >> (8 * (@csum_bytes - 1 - i))) & 0xFF }
|
|
98
|
+
bytes.pack('C*')
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# The +len+ base w digits signed for +msg+: the message digits followed by the checksum digits.
|
|
102
|
+
# @param [String] msg Binary string of +n+ bytes.
|
|
103
|
+
# @return [Array<Integer>]
|
|
104
|
+
# @raise ArgumentError
|
|
105
|
+
def digits(msg)
|
|
106
|
+
check_value!(msg, 'msg')
|
|
107
|
+
msg_digits = base_w(msg, len1)
|
|
108
|
+
msg_digits + base_w(checksum_bytes(msg_digits), len2)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Walk +steps+ steps of chain +chain_idx+ from position +start+.
|
|
112
|
+
# @param hasher
|
|
113
|
+
# @param [String] x Binary string of +n+ bytes, the value at position +start+.
|
|
114
|
+
# @param [Integer] chain_idx
|
|
115
|
+
# @param [Integer] start
|
|
116
|
+
# @param [Integer] steps
|
|
117
|
+
# @return [String] Binary string of +n+ bytes, the value at position +start+ + +steps+.
|
|
118
|
+
# @raise ArgumentError
|
|
119
|
+
def chain(hasher, x, chain_idx, start, steps)
|
|
120
|
+
check_value!(x, 'x')
|
|
121
|
+
raise ArgumentError, 'chain_idx must be integer.' unless chain_idx.is_a?(Integer)
|
|
122
|
+
raise ArgumentError, "chain_idx must be between 0 and #{len - 1}." unless chain_idx >= 0 && chain_idx < len
|
|
123
|
+
raise ArgumentError, 'start must be integer.' unless start.is_a?(Integer)
|
|
124
|
+
raise ArgumentError, 'steps must be integer.' unless steps.is_a?(Integer)
|
|
125
|
+
raise ArgumentError, 'start must not be negative.' if start.negative?
|
|
126
|
+
raise ArgumentError, 'steps must not be negative.' if steps.negative?
|
|
127
|
+
raise ArgumentError, 'Invalid range' if start + steps > w - 1
|
|
128
|
+
|
|
129
|
+
steps.times do |j|
|
|
130
|
+
x = hasher.step(x, chain_idx, start + j)
|
|
131
|
+
end
|
|
132
|
+
check_value!(x, 'hasher#step result') if steps.positive?
|
|
133
|
+
x
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Generate the public key (RFC 8391 WOTS_genPK, FIPS 205 Algorithm 6).
|
|
137
|
+
# @param hasher
|
|
138
|
+
# @return The result of +hasher.compress+.
|
|
139
|
+
def pk_gen(hasher)
|
|
140
|
+
values = Array.new(len) do |i|
|
|
141
|
+
chain(hasher, secret(hasher, i), i, 0, w - 1)
|
|
142
|
+
end
|
|
143
|
+
hasher.compress(values)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Sign +msg+ (RFC 8391 WOTS_sign, FIPS 205 Algorithm 7).
|
|
147
|
+
# @param hasher
|
|
148
|
+
# @param [String] msg Binary string of +n+ bytes.
|
|
149
|
+
# @return [Array<String>] +len+ binary strings of +n+ bytes.
|
|
150
|
+
# @raise ArgumentError
|
|
151
|
+
def sign(hasher, msg)
|
|
152
|
+
ds = digits(msg)
|
|
153
|
+
Array.new(len) do |i|
|
|
154
|
+
chain(hasher, secret(hasher, i), i, 0, ds[i])
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Compute the public key from a signature (RFC 8391 WOTS_pkFromSig, FIPS 205 Algorithm 8).
|
|
159
|
+
# @param hasher
|
|
160
|
+
# @param [Array<String>] sig +len+ binary strings of +n+ bytes.
|
|
161
|
+
# @param [String] msg Binary string of +n+ bytes.
|
|
162
|
+
# @return The result of +hasher.compress+.
|
|
163
|
+
# @raise ArgumentError
|
|
164
|
+
def pk_from_sig(hasher, sig, msg)
|
|
165
|
+
raise ArgumentError, 'sig must be Array.' unless sig.is_a?(Array)
|
|
166
|
+
raise ArgumentError, "sig must have #{len} elements." unless sig.length == len
|
|
167
|
+
|
|
168
|
+
sig.each { |s| check_value!(s, 'sig element') }
|
|
169
|
+
ds = digits(msg)
|
|
170
|
+
values = Array.new(len) do |i|
|
|
171
|
+
chain(hasher, sig[i], i, ds[i], w - 1 - ds[i])
|
|
172
|
+
end
|
|
173
|
+
hasher.compress(values)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
private
|
|
177
|
+
|
|
178
|
+
def secret(hasher, chain_idx)
|
|
179
|
+
sk = hasher.secret(chain_idx)
|
|
180
|
+
check_value!(sk, 'hasher#secret result')
|
|
181
|
+
sk
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def check_value!(value, name)
|
|
185
|
+
raise ArgumentError, "#{name} must be string." unless value.is_a?(String)
|
|
186
|
+
raise ArgumentError, "#{name} must be #{n} bytes." unless value.bytesize == n
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
data/lib/wots/param.rb
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
require '
|
|
1
|
+
require 'sha3'
|
|
2
2
|
|
|
3
3
|
module WOTS
|
|
4
4
|
# WOTS+ parameters defined in rfc8391.
|
|
@@ -11,6 +11,7 @@ module WOTS
|
|
|
11
11
|
autoload :SHAKE256, 'wots/param/shake256'
|
|
12
12
|
|
|
13
13
|
# The parameter sets defined in RFC 8391 and the +n+ they require.
|
|
14
|
+
# WOTSP-SHAKE_256 uses SHAKE128 with a 256-bit output (RFC 8391 Section 5.1 and 5.2).
|
|
14
15
|
SUPPORTED_NAMES = {
|
|
15
16
|
'WOTSP-SHA2_256' => 32,
|
|
16
17
|
'WOTSP-SHA2_512' => 64,
|
|
@@ -18,12 +19,35 @@ module WOTS
|
|
|
18
19
|
}.freeze
|
|
19
20
|
|
|
20
21
|
# The Winternitz parameter is a member of the set {4, 16}.
|
|
21
|
-
# Other values break the checksum encoding
|
|
22
|
-
SUPPORTED_W =
|
|
22
|
+
# Other values break the checksum encoding and allow signature forgery.
|
|
23
|
+
SUPPORTED_W = Core::SUPPORTED_W
|
|
24
|
+
|
|
25
|
+
# Domain separation prefixes of the keyed hash functions (RFC 8391 Section 5.1,
|
|
26
|
+
# and NIST SP 800-208 Section 5 for PRF_KEYGEN).
|
|
27
|
+
F = 0
|
|
28
|
+
H = 1
|
|
29
|
+
H_MSG = 2
|
|
30
|
+
PRF = 3
|
|
31
|
+
PRF_KEYGEN = 4
|
|
32
|
+
|
|
33
|
+
# The byte lengths of KEY and M accepted by each keyed hash function, as multiples of n
|
|
34
|
+
# plus a constant: [[key_n, key_extra], [message_n, message_extra]], or nil for any length.
|
|
35
|
+
# RFC 8391 Section 3.1.1.1 (F, PRF), Section 4.1.2 (H, H_msg), NIST SP 800-208 Section 6.2
|
|
36
|
+
# (PRF_keygen, whose M is SEED || ADRS).
|
|
37
|
+
INPUT_LENGTHS = {
|
|
38
|
+
F => [[1, 0], [1, 0]],
|
|
39
|
+
H => [[1, 0], [2, 0]],
|
|
40
|
+
H_MSG => [[3, 0], nil],
|
|
41
|
+
PRF => [[1, 0], [0, 32]],
|
|
42
|
+
PRF_KEYGEN => [[1, 0], [1, 32]]
|
|
43
|
+
}.freeze
|
|
44
|
+
private_constant :INPUT_LENGTHS
|
|
23
45
|
|
|
24
46
|
attr_reader :name
|
|
25
47
|
attr_reader :n
|
|
26
48
|
attr_reader :w
|
|
49
|
+
# @return [WOTS::Core] The WOTS+ engine for this parameter set.
|
|
50
|
+
attr_reader :core
|
|
27
51
|
|
|
28
52
|
# @param [Hash] opts
|
|
29
53
|
# @option opts [String] :name the name of the parameter set; it is a member of +SUPPORTED_NAMES+.
|
|
@@ -41,56 +65,103 @@ module WOTS
|
|
|
41
65
|
raise ArgumentError, "n must be #{expected_n} for #{opts[:name]}." unless opts[:n] == expected_n
|
|
42
66
|
raise ArgumentError, "w must be one of #{SUPPORTED_W.join(', ')}." unless SUPPORTED_W.include?(opts[:w])
|
|
43
67
|
|
|
44
|
-
@name = opts[:name]
|
|
68
|
+
@name = opts[:name].dup.freeze
|
|
45
69
|
@n = opts[:n]
|
|
46
70
|
@w = opts[:w]
|
|
71
|
+
@core = Core.new(n: n, w: w)
|
|
72
|
+
freeze
|
|
47
73
|
end
|
|
48
74
|
|
|
49
75
|
# log2(w), i.e. the number of bits consumed by a single base w digit.
|
|
50
76
|
# @return [Integer]
|
|
51
77
|
def lg_w
|
|
52
|
-
|
|
78
|
+
core.lg_w
|
|
53
79
|
end
|
|
54
80
|
|
|
55
81
|
def len1
|
|
56
|
-
|
|
82
|
+
core.len1
|
|
57
83
|
end
|
|
58
84
|
|
|
59
85
|
def len2
|
|
60
|
-
|
|
86
|
+
core.len2
|
|
61
87
|
end
|
|
62
88
|
|
|
63
89
|
def len
|
|
64
|
-
|
|
90
|
+
core.len
|
|
65
91
|
end
|
|
66
92
|
|
|
67
|
-
# @param [String] k Hex string.
|
|
68
|
-
# @param [String] m Hex string.
|
|
93
|
+
# @param [String] k Hex string of +n+ bytes.
|
|
94
|
+
# @param [String] m Hex string of +n+ bytes.
|
|
69
95
|
# @return [String] Hex string.
|
|
70
96
|
def f(k, m)
|
|
71
|
-
keyed_hash(
|
|
97
|
+
keyed_hash(F, k, m)
|
|
72
98
|
end
|
|
73
99
|
|
|
74
|
-
# @param [String] k Hex string.
|
|
75
|
-
# @param [String] m Hex string.
|
|
100
|
+
# @param [String] k Hex string of +n+ bytes.
|
|
101
|
+
# @param [String] m Hex string of 2n bytes.
|
|
76
102
|
# @return [String] Hex string.
|
|
77
103
|
def h(k, m)
|
|
78
|
-
keyed_hash(
|
|
104
|
+
keyed_hash(H, k, m)
|
|
79
105
|
end
|
|
80
106
|
|
|
81
|
-
# @param [String] k Hex string.
|
|
107
|
+
# @param [String] k Hex string of 3n bytes.
|
|
82
108
|
# @param [String] m Hex string.
|
|
83
109
|
# @return [String] Hex string.
|
|
84
110
|
def h_msg(k, m)
|
|
85
|
-
keyed_hash(
|
|
111
|
+
keyed_hash(H_MSG, k, m)
|
|
86
112
|
end
|
|
87
113
|
|
|
88
114
|
# PRF function.
|
|
89
|
-
# @param [String] k key Hex string.
|
|
90
|
-
# @param [String] m message Hex string.
|
|
115
|
+
# @param [String] k key Hex string of +n+ bytes.
|
|
116
|
+
# @param [String] m message Hex string of 32 bytes.
|
|
91
117
|
# @return [String] Hex string.
|
|
92
118
|
def prf(k, m)
|
|
93
|
-
keyed_hash(
|
|
119
|
+
keyed_hash(PRF, k, m)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# PRF used for pseudorandom key generation (NIST SP 800-208 Section 5).
|
|
123
|
+
# RFC 8391 does not define it; SP 800-208 defines it with the domain separation prefix 4,
|
|
124
|
+
# which is also what the XMSS reference implementation uses for every parameter set.
|
|
125
|
+
# @param [String] k key Hex string of +n+ bytes.
|
|
126
|
+
# @param [String] m message Hex string of n + 32 bytes (SEED || ADRS).
|
|
127
|
+
# @return [String] Hex string.
|
|
128
|
+
def prf_keygen(k, m)
|
|
129
|
+
keyed_hash(PRF_KEYGEN, k, m)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# The keyed hash functions of RFC 8391 Section 5.1 on binary strings:
|
|
133
|
+
# Hash(toByte(prefix, n) || KEY || M).
|
|
134
|
+
# @param [Integer] prefix One of F, H, H_MSG, PRF and PRF_KEYGEN.
|
|
135
|
+
# @param [String] key Binary string; +n+ bytes, or 3n bytes for H_MSG.
|
|
136
|
+
# @param [String] message Binary string; n bytes for F, 2n for H, any length for H_MSG,
|
|
137
|
+
# 32 for PRF and n + 32 for PRF_KEYGEN.
|
|
138
|
+
# @return [String] Binary string of +n+ bytes.
|
|
139
|
+
# @raise ArgumentError If +prefix+ is unknown or +key+ or +message+ has the wrong length.
|
|
140
|
+
def keyed_digest(prefix, key, message)
|
|
141
|
+
lengths = INPUT_LENGTHS[prefix]
|
|
142
|
+
raise ArgumentError, 'prefix must be one of F, H, H_MSG, PRF and PRF_KEYGEN.' unless lengths
|
|
143
|
+
raise ArgumentError, 'key must be string.' unless key.is_a?(String)
|
|
144
|
+
raise ArgumentError, 'message must be string.' unless message.is_a?(String)
|
|
145
|
+
|
|
146
|
+
key_len = lengths[0][0] * n + lengths[0][1]
|
|
147
|
+
raise ArgumentError, "key must be #{key_len} bytes." unless key.bytesize == key_len
|
|
148
|
+
if lengths[1]
|
|
149
|
+
message_len = lengths[1][0] * n + lengths[1][1]
|
|
150
|
+
raise ArgumentError, "message must be #{message_len} bytes." unless message.bytesize == message_len
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
payload = to_byte(prefix, n) + key.b + message.b
|
|
154
|
+
case name
|
|
155
|
+
when 'WOTSP-SHA2_256'
|
|
156
|
+
Digest::SHA256.digest(payload)
|
|
157
|
+
when 'WOTSP-SHA2_512'
|
|
158
|
+
Digest::SHA512.digest(payload)
|
|
159
|
+
when 'WOTSP-SHAKE_256'
|
|
160
|
+
# RFC 8391 Section 5.1: SHAKE128(toByte(prefix, 32) || KEY || M, 256)
|
|
161
|
+
SHA3::Digest::SHAKE_128.new(payload).squeeze(n)
|
|
162
|
+
else
|
|
163
|
+
raise 'Unknown param'
|
|
164
|
+
end
|
|
94
165
|
end
|
|
95
166
|
|
|
96
167
|
# Convert data as base w representation.
|
|
@@ -99,46 +170,18 @@ module WOTS
|
|
|
99
170
|
# @return [Array] An array of integer.
|
|
100
171
|
# @raise ArgumentError If +data+ is too short to produce +out_len+ digits.
|
|
101
172
|
def base_w(data, out_len)
|
|
102
|
-
|
|
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
|
|
108
|
-
|
|
109
|
-
basew = []
|
|
110
|
-
in_idx = 0
|
|
111
|
-
total = 0
|
|
112
|
-
bits = 0
|
|
113
|
-
|
|
114
|
-
out_len.times do
|
|
115
|
-
if bits == 0
|
|
116
|
-
total = x.getbyte(in_idx)
|
|
117
|
-
in_idx += 1
|
|
118
|
-
bits += 8
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
bits -= lg_w
|
|
122
|
-
basew << ((total >> bits) & (w - 1))
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
basew
|
|
173
|
+
core.base_w(hex_to_bin(data), out_len)
|
|
126
174
|
end
|
|
127
175
|
|
|
128
176
|
# Compute checksum for +base_w+.
|
|
129
177
|
# @param [Array] base_w
|
|
130
178
|
# @return [String] Checksum hex string.
|
|
131
179
|
def compute_checksum(base_w)
|
|
132
|
-
|
|
133
|
-
len1.times do |i|
|
|
134
|
-
c_sum = (c_sum + w - 1 - base_w[i])
|
|
135
|
-
end
|
|
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))
|
|
180
|
+
bin_to_hex(core.checksum_bytes(base_w))
|
|
139
181
|
end
|
|
140
182
|
|
|
141
183
|
# WOTS+ Chaining Function.
|
|
184
|
+
# The chain address is taken from +addr+; +addr+ itself is not modified.
|
|
142
185
|
# @see https://datatracker.ietf.org/doc/html/rfc8391#autoid-16
|
|
143
186
|
# @param [String] x Input string. Hex string.
|
|
144
187
|
# @param [Integer] start_idx Start index.
|
|
@@ -149,30 +192,11 @@ module WOTS
|
|
|
149
192
|
# @raise ArgumentError
|
|
150
193
|
def chain(x, start_idx, steps, seed, addr)
|
|
151
194
|
raise ArgumentError, 'x must be hex string.' unless hex_string?(x)
|
|
152
|
-
raise ArgumentError, '
|
|
153
|
-
raise ArgumentError, '
|
|
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
|
-
|
|
158
|
-
return x if steps == 0
|
|
159
|
-
|
|
160
|
-
result = x.dup
|
|
161
|
-
|
|
162
|
-
steps.times do |i|
|
|
163
|
-
addr.hash_addr = (start_idx + i)
|
|
164
|
-
|
|
165
|
-
addr.key_and_mask = 0
|
|
166
|
-
key = prf(seed, bin_to_hex(addr.to_payload))
|
|
195
|
+
raise ArgumentError, 'seed must be hex string.' unless hex_string?(seed)
|
|
196
|
+
raise ArgumentError, 'addr must be WOTS::Address.' unless addr.is_a?(Address)
|
|
167
197
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
masked = xor_bytes(result, mask)
|
|
172
|
-
result = f(key, masked)
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
result
|
|
198
|
+
hasher = RFC8391Hasher.new(self, pub_seed: hex_to_bin(seed), addr: addr)
|
|
199
|
+
bin_to_hex(core.chain(hasher, hex_to_bin(x), addr.chain_addr, start_idx, steps))
|
|
176
200
|
end
|
|
177
201
|
|
|
178
202
|
def ==(other)
|
|
@@ -203,34 +227,12 @@ module WOTS
|
|
|
203
227
|
|
|
204
228
|
private
|
|
205
229
|
|
|
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.
|
|
210
|
-
def xor_bytes(a, b)
|
|
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*"))
|
|
216
|
-
end
|
|
217
|
-
|
|
218
230
|
# @param [Integer] prefix Domain separation prefix.
|
|
219
231
|
# @param [String] k Hex string.
|
|
220
232
|
# @param [String] m Hex string.
|
|
221
233
|
# @return [String] Hex string.
|
|
222
234
|
def keyed_hash(prefix, k, m)
|
|
223
|
-
|
|
224
|
-
case name
|
|
225
|
-
when 'WOTSP-SHA2_256'
|
|
226
|
-
Digest::SHA256.hexdigest(payload)
|
|
227
|
-
when 'WOTSP-SHA2_512'
|
|
228
|
-
Digest::SHA512.hexdigest(payload)
|
|
229
|
-
when 'WOTSP-SHAKE_256'
|
|
230
|
-
OpenSSL::Digest.new('shake256', payload).hexdigest
|
|
231
|
-
else
|
|
232
|
-
raise 'Unknown param'
|
|
233
|
-
end
|
|
235
|
+
bin_to_hex(keyed_digest(prefix, hex_to_bin(k), hex_to_bin(m)))
|
|
234
236
|
end
|
|
235
237
|
end
|
|
236
238
|
end
|
data/lib/wots/private_key.rb
CHANGED
|
@@ -17,54 +17,55 @@ module WOTS
|
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
@param = param
|
|
20
|
-
@keys = keys
|
|
20
|
+
@keys = normalize_hex_array(keys)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
# Generate private key using +seed+.
|
|
24
|
+
# Each element is derived as sk[i] = PRF_keygen(seed, pub_seed || ADRS) with the
|
|
25
|
+
# chain address of ADRS set to i (NIST SP 800-208 Section 6.2, Algorithm 10').
|
|
26
|
+
# Binding the key to +pub_seed+ and +addr+ gives every key pair and every OTS address
|
|
27
|
+
# its own private key, so one +seed+ can derive several keys by using a different
|
|
28
|
+
# +addr+ (e.g. +ots_addr+) for each of them.
|
|
24
29
|
# @param [WOTS::Param] param
|
|
25
30
|
# @param [String] seed The secret seed. Hex string of +param.n+ bytes.
|
|
26
31
|
# It must be generated with a cryptographically secure random number generator.
|
|
32
|
+
# @param [String] pub_seed The public seed. Hex string of +param.n+ bytes.
|
|
33
|
+
# @param [WOTS::Address] addr The address of this key pair. The same address must be passed
|
|
34
|
+
# to #sign, PublicKey.from_private_key and PublicKey.from_signature.
|
|
35
|
+
# @return [WOTS::PrivateKey]
|
|
27
36
|
# @raise ArgumentError
|
|
28
|
-
def self.from_seed(param, seed)
|
|
37
|
+
def self.from_seed(param, seed, pub_seed, addr: Address.new)
|
|
29
38
|
raise ArgumentError, "param must be WOTS::Param." unless param.is_a?(WOTS::Param)
|
|
30
39
|
raise ArgumentError, "seed must be hex string." unless hex_string?(seed)
|
|
31
40
|
raise ArgumentError, "seed must be #{param.n} bytes." unless hex_to_bin(seed).bytesize == param.n
|
|
32
|
-
raise ArgumentError,
|
|
41
|
+
raise ArgumentError, 'pub_seed must be hex string.' unless hex_string?(pub_seed)
|
|
42
|
+
raise ArgumentError, "pub_seed must be #{param.n} bytes." unless hex_to_bin(pub_seed).bytesize == param.n
|
|
33
43
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
end
|
|
44
|
+
hasher = RFC8391Hasher.new(param, pub_seed: hex_to_bin(pub_seed), addr: addr, seed: hex_to_bin(seed))
|
|
45
|
+
keys = Array.new(param.len) { |i| bin_to_hex(hasher.secret(i)) }
|
|
37
46
|
|
|
38
47
|
PrivateKey.new(param, keys)
|
|
39
48
|
end
|
|
40
49
|
|
|
41
50
|
# Generate signature.
|
|
42
|
-
# WOTS+ is a one-time signature scheme: this private key must
|
|
43
|
-
#
|
|
51
|
+
# WOTS+ is a one-time signature scheme: this private key must be used to sign
|
|
52
|
+
# one message, once. This library does not track whether a key has been used;
|
|
53
|
+
# the caller must persist its state (e.g. the next address) to non-volatile
|
|
54
|
+
# storage before releasing the signature. See "One-time keys" in README.md.
|
|
44
55
|
# @param [String] pub_seed The Public seed. Hex string of +param.n+ bytes.
|
|
45
56
|
# @param [String] message The message to be signed. Hex string of +param.n+ bytes.
|
|
57
|
+
# @param [WOTS::Address] addr The address passed to PrivateKey.from_seed.
|
|
46
58
|
# @return [WOTS::Signature]
|
|
47
59
|
# @raise ArgumentError
|
|
48
|
-
def sign(pub_seed, message)
|
|
60
|
+
def sign(pub_seed, message, addr: Address.new)
|
|
49
61
|
raise ArgumentError, 'pub_seed must be hex string.' unless hex_string?(pub_seed)
|
|
50
62
|
raise ArgumentError, "pub_seed must be #{param.n} bytes." unless hex_to_bin(pub_seed).bytesize == param.n
|
|
51
63
|
raise ArgumentError, "message must be hex string." unless hex_string?(message)
|
|
52
64
|
raise ArgumentError, "message must be #{param.n} bytes." unless hex_to_bin(message).bytesize == param.n
|
|
53
65
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
base_w = param.base_w(message, param.len1)
|
|
58
|
-
|
|
59
|
-
# Compute checksum
|
|
60
|
-
c_sum = param.compute_checksum(base_w)
|
|
61
|
-
|
|
62
|
-
base_w = base_w + param.base_w(c_sum, param.len2)
|
|
63
|
-
|
|
64
|
-
sigs = param.len.times.map do |i|
|
|
65
|
-
addr.chain_addr = i
|
|
66
|
-
param.chain(keys[i], 0, base_w[i], pub_seed, addr)
|
|
67
|
-
end
|
|
66
|
+
hasher = RFC8391Hasher.new(param, pub_seed: hex_to_bin(pub_seed), addr: addr,
|
|
67
|
+
keys: keys.map { |k| hex_to_bin(k) })
|
|
68
|
+
sigs = param.core.sign(hasher, hex_to_bin(message)).map { |s| bin_to_hex(s) }
|
|
68
69
|
|
|
69
70
|
Signature.new(param, sigs)
|
|
70
71
|
end
|
data/lib/wots/public_key.rb
CHANGED
|
@@ -16,34 +16,33 @@ module WOTS
|
|
|
16
16
|
raise ArgumentError, "key must be #{param.n} bytes." unless hex_to_bin(key).bytesize == param.n
|
|
17
17
|
end
|
|
18
18
|
@param = param
|
|
19
|
-
@keys = keys
|
|
19
|
+
@keys = normalize_hex_array(keys)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
# Generate public key from +private_key+ and +pub_seed+.
|
|
23
23
|
# @param [WOTS::PrivateKey] private_key
|
|
24
24
|
# @param [String] pub_seed Hex string of +param.n+ bytes.
|
|
25
|
+
# @param [WOTS::Address] addr The address passed to PrivateKey.from_seed.
|
|
25
26
|
# @raise ArgumentError
|
|
26
|
-
def self.from_private_key(private_key, pub_seed)
|
|
27
|
+
def self.from_private_key(private_key, pub_seed, addr: Address.new)
|
|
27
28
|
raise ArgumentError, 'private_key must be WOTS::PrivateKey.' unless private_key.is_a?(WOTS::PrivateKey)
|
|
28
29
|
param = private_key.param
|
|
29
30
|
raise ArgumentError, 'pub_seed must be hex string.' unless hex_string?(pub_seed)
|
|
30
31
|
raise ArgumentError, "pub_seed must be #{param.n} bytes." unless hex_to_bin(pub_seed).bytesize == param.n
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
param.chain(sk, 0, param.w - 1, pub_seed, addr)
|
|
36
|
-
end
|
|
37
|
-
PublicKey.new(private_key.param, keys)
|
|
32
|
+
hasher = RFC8391Hasher.new(param, pub_seed: hex_to_bin(pub_seed), addr: addr,
|
|
33
|
+
keys: private_key.keys.map { |k| hex_to_bin(k) })
|
|
34
|
+
keys = param.core.pk_gen(hasher).map { |k| bin_to_hex(k) }
|
|
35
|
+
PublicKey.new(param, keys)
|
|
38
36
|
end
|
|
39
37
|
|
|
40
38
|
# Generate public key from +signature+.
|
|
41
39
|
# @param [WOTS::Signature] signature
|
|
42
40
|
# @param [String] pub_seed Hex string of +param.n+ bytes.
|
|
43
41
|
# @param [String] message Hex string of +param.n+ bytes.
|
|
42
|
+
# @param [WOTS::Address] addr The address passed to PrivateKey.from_seed.
|
|
44
43
|
# @return [WOTS::PublicKey]
|
|
45
44
|
# @raise ArgumentError
|
|
46
|
-
def self.from_signature(signature, pub_seed, message)
|
|
45
|
+
def self.from_signature(signature, pub_seed, message, addr: Address.new)
|
|
47
46
|
raise ArgumentError, 'signature must be WOTS::Signature.' unless signature.is_a?(WOTS::Signature)
|
|
48
47
|
param = signature.param
|
|
49
48
|
raise ArgumentError, 'pub_seed must be hex string.' unless hex_string?(pub_seed)
|
|
@@ -51,23 +50,37 @@ module WOTS
|
|
|
51
50
|
raise ArgumentError, "message must be hex string." unless hex_string?(message)
|
|
52
51
|
raise ArgumentError, "message must be #{param.n} bytes." unless hex_to_bin(message).bytesize == param.n
|
|
53
52
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
hasher = RFC8391Hasher.new(param, pub_seed: hex_to_bin(pub_seed), addr: addr)
|
|
54
|
+
sigs = signature.sigs.map { |s| hex_to_bin(s) }
|
|
55
|
+
keys = param.core.pk_from_sig(hasher, sigs, hex_to_bin(message)).map { |k| bin_to_hex(k) }
|
|
57
56
|
|
|
58
|
-
|
|
57
|
+
PublicKey.new(param, keys)
|
|
58
|
+
end
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
# Verify +signature+ on +message+.
|
|
61
|
+
# Invalid input (wrong types or sizes) raises ArgumentError; a well-formed signature
|
|
62
|
+
# that does not verify, including one made with another parameter set, returns false.
|
|
63
|
+
# @param [WOTS::Signature] signature
|
|
64
|
+
# @param [String] pub_seed Hex string of +param.n+ bytes.
|
|
65
|
+
# @param [String] message Hex string of +param.n+ bytes.
|
|
66
|
+
# @param [WOTS::Address] addr The address passed to PrivateKey.from_seed.
|
|
67
|
+
# @return [Boolean]
|
|
68
|
+
# @raise ArgumentError
|
|
69
|
+
def verify(signature, pub_seed, message, addr: Address.new)
|
|
70
|
+
raise ArgumentError, 'signature must be WOTS::Signature.' unless signature.is_a?(WOTS::Signature)
|
|
71
|
+
return false unless signature.param == param
|
|
64
72
|
|
|
65
|
-
PublicKey.
|
|
73
|
+
PublicKey.from_signature(signature, pub_seed, message, addr: addr) == self
|
|
66
74
|
end
|
|
67
75
|
|
|
68
76
|
def ==(other)
|
|
69
77
|
return false unless other.is_a?(PublicKey)
|
|
70
78
|
param == other.param && keys == other.keys
|
|
71
79
|
end
|
|
80
|
+
alias eql? ==
|
|
81
|
+
|
|
82
|
+
def hash
|
|
83
|
+
[self.class, param.name, param.w, keys].hash
|
|
84
|
+
end
|
|
72
85
|
end
|
|
73
86
|
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
module WOTS
|
|
2
|
+
# WOTS::Core hasher for the WOTS+ of RFC 8391, bound to one key pair.
|
|
3
|
+
#
|
|
4
|
+
# * secret: PRF_keygen(seed, pub_seed || ADRS) (NIST SP 800-208 Section 6.2), or the
|
|
5
|
+
# stored private key elements when the key was given explicitly.
|
|
6
|
+
# * step: RFC 8391 Algorithm 2 (chain), i.e. F(KEY, X xor BM) with KEY and BM derived by PRF.
|
|
7
|
+
# * compress: the chain end values as they are (an RFC 8391 WOTS+ public key is the array).
|
|
8
|
+
#
|
|
9
|
+
# All values are binary strings.
|
|
10
|
+
class RFC8391Hasher
|
|
11
|
+
# @param [WOTS::Param] param
|
|
12
|
+
# @param [String] pub_seed Binary string of +param.n+ bytes.
|
|
13
|
+
# @param [WOTS::Address] addr The address of the key pair. It is copied, never modified.
|
|
14
|
+
# @param [String] seed Optional secret seed (binary string of +param.n+ bytes).
|
|
15
|
+
# @param [Array<String>] keys Optional private key elements (+param.len+ binary strings).
|
|
16
|
+
# Give at most one of +seed+ and +keys+; with neither, the hasher can only verify.
|
|
17
|
+
# @raise ArgumentError
|
|
18
|
+
def initialize(param, pub_seed:, addr:, seed: nil, keys: nil)
|
|
19
|
+
raise ArgumentError, 'param must be WOTS::Param.' unless param.is_a?(WOTS::Param)
|
|
20
|
+
raise ArgumentError, 'give at most one of seed and keys.' if seed && keys
|
|
21
|
+
|
|
22
|
+
check_value!(param, pub_seed, 'pub_seed')
|
|
23
|
+
check_value!(param, seed, 'seed') if seed
|
|
24
|
+
if keys
|
|
25
|
+
raise ArgumentError, 'keys must be Array.' unless keys.is_a?(Array)
|
|
26
|
+
raise ArgumentError, "keys must have #{param.len} elements." unless keys.length == param.len
|
|
27
|
+
|
|
28
|
+
keys.each { |k| check_value!(param, k, 'key') }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
@param = param
|
|
32
|
+
@pub_seed = pub_seed.b.freeze
|
|
33
|
+
@seed = seed&.b&.freeze
|
|
34
|
+
@keys = keys&.map { |k| k.b.freeze }&.freeze
|
|
35
|
+
@addr = Address.copy_of(addr)
|
|
36
|
+
@keygen_addr = Address.copy_of(addr)
|
|
37
|
+
@keygen_addr.hash_addr = 0
|
|
38
|
+
@keygen_addr.key_and_mask = 0
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @param [Integer] chain_idx
|
|
42
|
+
# @return [String]
|
|
43
|
+
def secret(chain_idx)
|
|
44
|
+
check_index!(chain_idx, @param.len, 'chain_idx')
|
|
45
|
+
return @keys[chain_idx] if @keys
|
|
46
|
+
raise ArgumentError, 'this hasher has no secret seed or private key.' unless @seed
|
|
47
|
+
|
|
48
|
+
@keygen_addr.chain_addr = chain_idx
|
|
49
|
+
@param.keyed_digest(Param::PRF_KEYGEN, @seed, @pub_seed + @keygen_addr.to_payload)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @param [String] x
|
|
53
|
+
# @param [Integer] chain_idx
|
|
54
|
+
# @param [Integer] hash_idx
|
|
55
|
+
# @return [String]
|
|
56
|
+
def step(x, chain_idx, hash_idx)
|
|
57
|
+
check_value!(@param, x, 'x')
|
|
58
|
+
check_index!(chain_idx, @param.len, 'chain_idx')
|
|
59
|
+
check_index!(hash_idx, @param.w - 1, 'hash_idx')
|
|
60
|
+
|
|
61
|
+
@addr.chain_addr = chain_idx
|
|
62
|
+
@addr.hash_addr = hash_idx
|
|
63
|
+
|
|
64
|
+
@addr.key_and_mask = 0
|
|
65
|
+
key = @param.keyed_digest(Param::PRF, @pub_seed, @addr.to_payload)
|
|
66
|
+
|
|
67
|
+
@addr.key_and_mask = 1
|
|
68
|
+
mask = @param.keyed_digest(Param::PRF, @pub_seed, @addr.to_payload)
|
|
69
|
+
|
|
70
|
+
@param.keyed_digest(Param::F, key, xor(x, mask))
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# @param [Array<String>] values
|
|
74
|
+
# @return [Array<String>]
|
|
75
|
+
def compress(values)
|
|
76
|
+
values
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Redacted inspect, so that the secret seed and private key elements never reach
|
|
80
|
+
# logs, exception reports or REPL output.
|
|
81
|
+
# @return [String]
|
|
82
|
+
def inspect
|
|
83
|
+
secret = if @keys then 'keys' elsif @seed then 'seed' else 'none' end
|
|
84
|
+
"#<WOTS::RFC8391Hasher param=#{@param.name} w=#{@param.w} secret=#{secret} [REDACTED]>"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def xor(a, b)
|
|
90
|
+
raise ArgumentError, 'length mismatch.' unless a.bytesize == b.bytesize
|
|
91
|
+
|
|
92
|
+
a.bytes.zip(b.bytes).map { |i, j| i ^ j }.pack('C*')
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def check_index!(value, limit, name)
|
|
96
|
+
raise ArgumentError, "#{name} must be integer." unless value.is_a?(Integer)
|
|
97
|
+
raise ArgumentError, "#{name} must be between 0 and #{limit - 1}." unless value >= 0 && value < limit
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def check_value!(param, value, name)
|
|
101
|
+
raise ArgumentError, "#{name} must be string." unless value.is_a?(String)
|
|
102
|
+
raise ArgumentError, "#{name} must be #{param.n} bytes." unless value.bytesize == param.n
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
data/lib/wots/signature.rb
CHANGED
|
@@ -15,7 +15,17 @@ module WOTS
|
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
@param = param
|
|
18
|
-
@sigs = sigs
|
|
18
|
+
@sigs = normalize_hex_array(sigs)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def ==(other)
|
|
22
|
+
return false unless other.is_a?(Signature)
|
|
23
|
+
param == other.param && sigs == other.sigs
|
|
24
|
+
end
|
|
25
|
+
alias eql? ==
|
|
26
|
+
|
|
27
|
+
def hash
|
|
28
|
+
[self.class, param.name, param.w, sigs].hash
|
|
19
29
|
end
|
|
20
30
|
end
|
|
21
31
|
end
|
data/lib/wots/util.rb
CHANGED
|
@@ -3,12 +3,15 @@ module WOTS
|
|
|
3
3
|
|
|
4
4
|
# Check whether +data+ is hex string or not.
|
|
5
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.
|
|
6
|
+
# silently zero-pad it and make two distinct inputs collide. A string in an
|
|
7
|
+
# encoding that is not ASCII compatible (e.g. UTF-16) is not a hex string either.
|
|
7
8
|
# @param [String] data
|
|
8
9
|
# @return [Boolean]
|
|
9
10
|
# @raise [ArgumentError]
|
|
10
11
|
def hex_string?(data)
|
|
11
12
|
raise ArgumentError, 'data must be string' unless data.is_a?(String)
|
|
13
|
+
return false unless data.encoding.ascii_compatible?
|
|
14
|
+
|
|
12
15
|
data.length.even? && data.match?(/\A[0-9a-fA-F]+\z/)
|
|
13
16
|
end
|
|
14
17
|
|
|
@@ -21,6 +24,15 @@ module WOTS
|
|
|
21
24
|
[data].pack('H*')
|
|
22
25
|
end
|
|
23
26
|
|
|
27
|
+
# Normalize the validated hex strings in +values+ to lowercase and freeze them,
|
|
28
|
+
# so that equality does not depend on letter case and later changes to the
|
|
29
|
+
# caller's strings or array cannot affect an object that has validated them.
|
|
30
|
+
# @param [Array<String>] values Hex strings.
|
|
31
|
+
# @return [Array<String>] A frozen array of frozen lowercase hex strings.
|
|
32
|
+
def normalize_hex_array(values)
|
|
33
|
+
values.map { |v| v.downcase.freeze }.freeze
|
|
34
|
+
end
|
|
35
|
+
|
|
24
36
|
# Convert binary string +data+ to hex string.
|
|
25
37
|
# @param [String] data Binary string.
|
|
26
38
|
# @return [String] Hex string.
|
data/lib/wots/version.rb
CHANGED
data/lib/wots.rb
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
require 'digest'
|
|
3
3
|
require_relative "wots/version"
|
|
4
4
|
require_relative 'wots/util'
|
|
5
|
+
require_relative 'wots/core'
|
|
5
6
|
require_relative 'wots/param'
|
|
6
7
|
require_relative 'wots/address'
|
|
8
|
+
require_relative 'wots/rfc8391_hasher'
|
|
7
9
|
require_relative 'wots/private_key'
|
|
8
10
|
require_relative 'wots/public_key'
|
|
9
11
|
require_relative 'wots/signature'
|
metadata
CHANGED
|
@@ -1,14 +1,28 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: wots
|
|
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
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
-
dependencies:
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: sha3
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.2'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2.2'
|
|
12
26
|
description: A Ruby implementation of the Winternitz OTS (W-OTS+), as described in
|
|
13
27
|
RFC8391.
|
|
14
28
|
email:
|
|
@@ -20,18 +34,21 @@ files:
|
|
|
20
34
|
- ".rspec"
|
|
21
35
|
- ".ruby-gemset"
|
|
22
36
|
- ".ruby-version"
|
|
37
|
+
- CHANGELOG.md
|
|
23
38
|
- CODE_OF_CONDUCT.md
|
|
24
39
|
- LICENSE.txt
|
|
25
40
|
- README.md
|
|
26
41
|
- Rakefile
|
|
27
42
|
- lib/wots.rb
|
|
28
43
|
- lib/wots/address.rb
|
|
44
|
+
- lib/wots/core.rb
|
|
29
45
|
- lib/wots/param.rb
|
|
30
46
|
- lib/wots/param/sha256.rb
|
|
31
47
|
- lib/wots/param/sha512.rb
|
|
32
48
|
- lib/wots/param/shake256.rb
|
|
33
49
|
- lib/wots/private_key.rb
|
|
34
50
|
- lib/wots/public_key.rb
|
|
51
|
+
- lib/wots/rfc8391_hasher.rb
|
|
35
52
|
- lib/wots/signature.rb
|
|
36
53
|
- lib/wots/util.rb
|
|
37
54
|
- lib/wots/version.rb
|
|
@@ -42,7 +59,8 @@ licenses:
|
|
|
42
59
|
metadata:
|
|
43
60
|
homepage_uri: https://github.com/azuchi/wots
|
|
44
61
|
source_code_uri: https://github.com/azuchi/wots
|
|
45
|
-
changelog_uri: https://github.com/azuchi/wots
|
|
62
|
+
changelog_uri: https://github.com/azuchi/wots/blob/main/CHANGELOG.md
|
|
63
|
+
rubygems_mfa_required: 'true'
|
|
46
64
|
rdoc_options: []
|
|
47
65
|
require_paths:
|
|
48
66
|
- lib
|
|
@@ -57,7 +75,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
57
75
|
- !ruby/object:Gem::Version
|
|
58
76
|
version: '0'
|
|
59
77
|
requirements: []
|
|
60
|
-
rubygems_version:
|
|
78
|
+
rubygems_version: 3.6.7
|
|
61
79
|
specification_version: 4
|
|
62
80
|
summary: A Ruby implementation of the Winternitz OTS (W-OTS+), as described in RFC8391.
|
|
63
81
|
test_files: []
|