wots 0.1.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/.ruby-version +1 -1
- data/CHANGELOG.md +70 -0
- data/README.md +130 -11
- data/lib/wots/address.rb +65 -13
- data/lib/wots/core.rb +189 -0
- data/lib/wots/param.rb +143 -82
- data/lib/wots/private_key.rb +38 -26
- data/lib/wots/public_key.rb +38 -23
- data/lib/wots/rfc8391_hasher.rb +105 -0
- data/lib/wots/signature.rb +11 -1
- data/lib/wots/util.rb +24 -10
- data/lib/wots/version.rb +1 -1
- data/lib/wots.rb +2 -0
- metadata +21 -3
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/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
ruby-
|
|
1
|
+
ruby-4.0.0
|
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,18 +86,119 @@ 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
|
+
|
|
123
|
+
## Input format
|
|
124
|
+
|
|
125
|
+
All key material, seeds and messages are **even-length hexadecimal strings**. Raw binary strings and
|
|
126
|
+
odd-length hex strings are rejected with an `ArgumentError`; convert them with `unpack1('H*')` first.
|
|
127
|
+
|
|
128
|
+
```ruby
|
|
129
|
+
message = Digest::SHA256.hexdigest("some payload") # 64 hex chars = 32 bytes
|
|
130
|
+
signature = private_key.sign(pub_seed, message)
|
|
131
|
+
```
|
|
132
|
+
|
|
75
133
|
## Security Considerations
|
|
76
134
|
|
|
77
|
-
**⚠️ IMPORTANT: This is a
|
|
135
|
+
**⚠️ IMPORTANT: This is a cryptographic library implementation. While it follows RFC 8391 specifications,
|
|
78
136
|
it has not undergone formal security audits. Use at your own risk in production environments.**
|
|
79
137
|
|
|
80
|
-
|
|
81
|
-
|
|
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
|
|
164
|
+
|
|
165
|
+
The seed passed to `WOTS::PrivateKey.from_seed` is secret key material and must come from a
|
|
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.
|
|
171
|
+
|
|
172
|
+
Only the Winternitz parameters defined in RFC 8391 (`w = 4` and `w = 16`) are accepted. Other values
|
|
173
|
+
overflow the checksum encoding, which drops the high bits of the checksum and allows an attacker to
|
|
174
|
+
forge signatures without the private key.
|
|
175
|
+
|
|
176
|
+
Private key material is held in Ruby `String` objects and is not zeroized; it remains in memory until
|
|
177
|
+
garbage collected.
|
|
82
178
|
|
|
83
179
|
## Specifications
|
|
84
180
|
|
|
85
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
|
@@ -1,27 +1,79 @@
|
|
|
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
|
|
20
51
|
0
|
|
21
52
|
end
|
|
22
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
|
+
|
|
23
65
|
def to_payload
|
|
24
66
|
[layer_addr, tree_addr, type, ots_addr, chain_addr, hash_addr, key_and_mask].pack('NQ>NNNNN')
|
|
25
67
|
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
# @raise ArgumentError If +value+ would not survive the round trip through +pack+.
|
|
72
|
+
def check_range(name, value, max)
|
|
73
|
+
raise ArgumentError, "#{name} must be integer." unless value.is_a?(Integer)
|
|
74
|
+
raise ArgumentError, "#{name} must be between 0 and #{max}." unless value >= 0 && value <= max
|
|
75
|
+
|
|
76
|
+
value
|
|
77
|
+
end
|
|
26
78
|
end
|
|
27
|
-
end
|
|
79
|
+
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.
|
|
@@ -10,126 +10,193 @@ 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
|
+
# WOTSP-SHAKE_256 uses SHAKE128 with a 256-bit output (RFC 8391 Section 5.1 and 5.2).
|
|
15
|
+
SUPPORTED_NAMES = {
|
|
16
|
+
'WOTSP-SHA2_256' => 32,
|
|
17
|
+
'WOTSP-SHA2_512' => 64,
|
|
18
|
+
'WOTSP-SHAKE_256' => 32
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
# The Winternitz parameter is a member of the set {4, 16}.
|
|
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
|
|
45
|
+
|
|
13
46
|
attr_reader :name
|
|
14
47
|
attr_reader :n
|
|
15
48
|
attr_reader :w
|
|
49
|
+
# @return [WOTS::Core] The WOTS+ engine for this parameter set.
|
|
50
|
+
attr_reader :core
|
|
16
51
|
|
|
17
52
|
# @param [Hash] opts
|
|
53
|
+
# @option opts [String] :name the name of the parameter set; it is a member of +SUPPORTED_NAMES+.
|
|
18
54
|
# @option opts [Integer] :n the message length as well as the length of a private key,
|
|
19
55
|
# public key, or signature element in bytes.
|
|
20
56
|
# @option opts [Integer] :w the Winternitz parameter; it is a member of the set {4, 16}.
|
|
21
|
-
# @
|
|
57
|
+
# @raise ArgumentError
|
|
22
58
|
def initialize(opts)
|
|
23
59
|
raise ArgumentError, 'name must be string.' unless opts[:name].is_a?(String)
|
|
24
60
|
raise ArgumentError, 'n must be integer.' unless opts[:n].is_a?(Integer)
|
|
25
61
|
raise ArgumentError, 'w must be integer.' unless opts[:w].is_a?(Integer)
|
|
26
62
|
|
|
27
|
-
|
|
63
|
+
expected_n = SUPPORTED_NAMES[opts[:name]]
|
|
64
|
+
raise ArgumentError, "name must be one of #{SUPPORTED_NAMES.keys.join(', ')}." if expected_n.nil?
|
|
65
|
+
raise ArgumentError, "n must be #{expected_n} for #{opts[:name]}." unless opts[:n] == expected_n
|
|
66
|
+
raise ArgumentError, "w must be one of #{SUPPORTED_W.join(', ')}." unless SUPPORTED_W.include?(opts[:w])
|
|
67
|
+
|
|
68
|
+
@name = opts[:name].dup.freeze
|
|
28
69
|
@n = opts[:n]
|
|
29
70
|
@w = opts[:w]
|
|
71
|
+
@core = Core.new(n: n, w: w)
|
|
72
|
+
freeze
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# log2(w), i.e. the number of bits consumed by a single base w digit.
|
|
76
|
+
# @return [Integer]
|
|
77
|
+
def lg_w
|
|
78
|
+
core.lg_w
|
|
30
79
|
end
|
|
31
80
|
|
|
32
81
|
def len1
|
|
33
|
-
|
|
82
|
+
core.len1
|
|
34
83
|
end
|
|
35
84
|
|
|
36
85
|
def len2
|
|
37
|
-
|
|
86
|
+
core.len2
|
|
38
87
|
end
|
|
39
88
|
|
|
40
89
|
def len
|
|
41
|
-
|
|
90
|
+
core.len
|
|
42
91
|
end
|
|
43
92
|
|
|
93
|
+
# @param [String] k Hex string of +n+ bytes.
|
|
94
|
+
# @param [String] m Hex string of +n+ bytes.
|
|
95
|
+
# @return [String] Hex string.
|
|
44
96
|
def f(k, m)
|
|
45
|
-
keyed_hash(
|
|
97
|
+
keyed_hash(F, k, m)
|
|
46
98
|
end
|
|
47
99
|
|
|
100
|
+
# @param [String] k Hex string of +n+ bytes.
|
|
101
|
+
# @param [String] m Hex string of 2n bytes.
|
|
102
|
+
# @return [String] Hex string.
|
|
48
103
|
def h(k, m)
|
|
49
|
-
keyed_hash(
|
|
104
|
+
keyed_hash(H, k, m)
|
|
50
105
|
end
|
|
51
106
|
|
|
107
|
+
# @param [String] k Hex string of 3n bytes.
|
|
108
|
+
# @param [String] m Hex string.
|
|
109
|
+
# @return [String] Hex string.
|
|
52
110
|
def h_msg(k, m)
|
|
53
|
-
keyed_hash(
|
|
111
|
+
keyed_hash(H_MSG, k, m)
|
|
54
112
|
end
|
|
55
113
|
|
|
56
114
|
# PRF function.
|
|
57
|
-
# @param [String] k key
|
|
58
|
-
# @param [String] m message
|
|
115
|
+
# @param [String] k key Hex string of +n+ bytes.
|
|
116
|
+
# @param [String] m message Hex string of 32 bytes.
|
|
59
117
|
# @return [String] Hex string.
|
|
60
118
|
def prf(k, m)
|
|
61
|
-
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
|
|
62
165
|
end
|
|
63
166
|
|
|
64
167
|
# Convert data as base w representation.
|
|
65
|
-
# @param [String] data The data to be converted.
|
|
168
|
+
# @param [String] data The data to be converted. Hex string.
|
|
66
169
|
# @param [Integer] out_len Output length.
|
|
67
170
|
# @return [Array] An array of integer.
|
|
171
|
+
# @raise ArgumentError If +data+ is too short to produce +out_len+ digits.
|
|
68
172
|
def base_w(data, out_len)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
basew = []
|
|
72
|
-
in_idx = 0
|
|
73
|
-
total = 0
|
|
74
|
-
bits = 0
|
|
75
|
-
lg_w = Math.log2(w).to_i
|
|
76
|
-
|
|
77
|
-
out_len.times do
|
|
78
|
-
if bits == 0
|
|
79
|
-
break if in_idx >= x.bytesize
|
|
80
|
-
total = x.getbyte(in_idx)
|
|
81
|
-
in_idx += 1
|
|
82
|
-
bits += 8
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
bits -= lg_w
|
|
86
|
-
basew << ((total >> bits) & (w - 1))
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
basew
|
|
173
|
+
core.base_w(hex_to_bin(data), out_len)
|
|
90
174
|
end
|
|
91
175
|
|
|
92
176
|
# Compute checksum for +base_w+.
|
|
93
177
|
# @param [Array] base_w
|
|
94
|
-
# @return [String] Checksum
|
|
178
|
+
# @return [String] Checksum hex string.
|
|
95
179
|
def compute_checksum(base_w)
|
|
96
|
-
|
|
97
|
-
len1.times do |i|
|
|
98
|
-
c_sum = (c_sum + w - 1 - base_w[i])
|
|
99
|
-
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)
|
|
180
|
+
bin_to_hex(core.checksum_bytes(base_w))
|
|
103
181
|
end
|
|
104
182
|
|
|
105
183
|
# WOTS+ Chaining Function.
|
|
184
|
+
# The chain address is taken from +addr+; +addr+ itself is not modified.
|
|
106
185
|
# @see https://datatracker.ietf.org/doc/html/rfc8391#autoid-16
|
|
107
|
-
# @param [String] x Input string.
|
|
186
|
+
# @param [String] x Input string. Hex string.
|
|
108
187
|
# @param [Integer] start_idx Start index.
|
|
109
188
|
# @param [Integer] steps Number of steps.
|
|
110
|
-
# @param [String] seed Seed.
|
|
189
|
+
# @param [String] seed Seed. Hex string.
|
|
111
190
|
# @param [WOTS::Address] addr Address.
|
|
112
|
-
# @return [String]
|
|
191
|
+
# @return [String] Hex string.
|
|
192
|
+
# @raise ArgumentError
|
|
113
193
|
def chain(x, start_idx, steps, seed, addr)
|
|
114
|
-
|
|
115
|
-
raise
|
|
116
|
-
|
|
117
|
-
result = x.dup
|
|
118
|
-
|
|
119
|
-
steps.times do |i|
|
|
120
|
-
addr.hash_addr = (start_idx + i)
|
|
194
|
+
raise ArgumentError, 'x must be hex string.' unless hex_string?(x)
|
|
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)
|
|
121
197
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
addr.key_and_mask = 1
|
|
126
|
-
mask = prf(seed, addr.to_payload)
|
|
127
|
-
|
|
128
|
-
masked = xor_bytes(result, mask)
|
|
129
|
-
result = f(key, masked)
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
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))
|
|
133
200
|
end
|
|
134
201
|
|
|
135
202
|
def ==(other)
|
|
@@ -140,8 +207,14 @@ module WOTS
|
|
|
140
207
|
# Convert +value+ to +length+ size binary string.
|
|
141
208
|
# @param [Integer] value
|
|
142
209
|
# @param [Integer] length
|
|
143
|
-
# @return [String]
|
|
210
|
+
# @return [String] Binary string.
|
|
211
|
+
# @raise ArgumentError If +value+ does not fit in +length+ bytes.
|
|
144
212
|
def to_byte(value, length)
|
|
213
|
+
raise ArgumentError, 'value must be integer.' unless value.is_a?(Integer)
|
|
214
|
+
raise ArgumentError, 'length must be integer.' unless length.is_a?(Integer)
|
|
215
|
+
raise ArgumentError, 'value must not be negative.' if value.negative?
|
|
216
|
+
raise ArgumentError, "value does not fit in #{length} bytes." if value.bit_length > length * 8
|
|
217
|
+
|
|
145
218
|
bytes = []
|
|
146
219
|
|
|
147
220
|
length.times do
|
|
@@ -154,24 +227,12 @@ module WOTS
|
|
|
154
227
|
|
|
155
228
|
private
|
|
156
229
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
end
|
|
162
|
-
|
|
230
|
+
# @param [Integer] prefix Domain separation prefix.
|
|
231
|
+
# @param [String] k Hex string.
|
|
232
|
+
# @param [String] m Hex string.
|
|
233
|
+
# @return [String] Hex string.
|
|
163
234
|
def keyed_hash(prefix, k, m)
|
|
164
|
-
|
|
165
|
-
case name
|
|
166
|
-
when 'WOTSP-SHA2_256'
|
|
167
|
-
Digest::SHA256.hexdigest(payload)
|
|
168
|
-
when 'WOTSP-SHA2_512'
|
|
169
|
-
Digest::SHA512.hexdigest(payload)
|
|
170
|
-
when 'WOTSP-SHAKE_256'
|
|
171
|
-
OpenSSL::Digest.new('shake256', payload).hexdigest
|
|
172
|
-
else
|
|
173
|
-
raise 'Unknown param'
|
|
174
|
-
end
|
|
235
|
+
bin_to_hex(keyed_digest(prefix, hex_to_bin(k), hex_to_bin(m)))
|
|
175
236
|
end
|
|
176
237
|
end
|
|
177
|
-
end
|
|
238
|
+
end
|
data/lib/wots/private_key.rb
CHANGED
|
@@ -17,53 +17,65 @@ 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
|
-
# @param [String] seed
|
|
30
|
+
# @param [String] seed The secret seed. Hex string of +param.n+ bytes.
|
|
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]
|
|
26
36
|
# @raise ArgumentError
|
|
27
|
-
def self.from_seed(param, seed)
|
|
37
|
+
def self.from_seed(param, seed, pub_seed, addr: Address.new)
|
|
28
38
|
raise ArgumentError, "param must be WOTS::Param." unless param.is_a?(WOTS::Param)
|
|
29
|
-
raise ArgumentError, "seed must be
|
|
39
|
+
raise ArgumentError, "seed must be hex string." unless hex_string?(seed)
|
|
30
40
|
raise ArgumentError, "seed must be #{param.n} bytes." unless hex_to_bin(seed).bytesize == param.n
|
|
31
|
-
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
|
|
32
43
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
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)) }
|
|
36
46
|
|
|
37
47
|
PrivateKey.new(param, keys)
|
|
38
48
|
end
|
|
39
49
|
|
|
40
50
|
# Generate signature.
|
|
41
|
-
#
|
|
42
|
-
#
|
|
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.
|
|
55
|
+
# @param [String] pub_seed The Public seed. Hex string of +param.n+ bytes.
|
|
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.
|
|
43
58
|
# @return [WOTS::Signature]
|
|
44
59
|
# @raise ArgumentError
|
|
45
|
-
def sign(pub_seed, message)
|
|
60
|
+
def sign(pub_seed, message, addr: Address.new)
|
|
46
61
|
raise ArgumentError, 'pub_seed must be hex string.' unless hex_string?(pub_seed)
|
|
47
62
|
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
|
|
63
|
+
raise ArgumentError, "message must be hex string." unless hex_string?(message)
|
|
49
64
|
raise ArgumentError, "message must be #{param.n} bytes." unless hex_to_bin(message).bytesize == param.n
|
|
50
65
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
base_w = param.base_w(message, param.len1)
|
|
55
|
-
|
|
56
|
-
# Compute checksum
|
|
57
|
-
c_sum = param.compute_checksum(base_w)
|
|
58
|
-
|
|
59
|
-
base_w = base_w + param.base_w(c_sum, param.len2)
|
|
60
|
-
|
|
61
|
-
sigs = param.len.times.map do |i|
|
|
62
|
-
addr.chain_addr = i
|
|
63
|
-
param.chain(keys[i], 0, base_w[i], pub_seed, addr)
|
|
64
|
-
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) }
|
|
65
69
|
|
|
66
70
|
Signature.new(param, sigs)
|
|
67
71
|
end
|
|
72
|
+
|
|
73
|
+
# Redacted inspect.
|
|
74
|
+
# The default implementation prints every instance variable, which would
|
|
75
|
+
# write the whole private key into logs, exception reports and REPL output.
|
|
76
|
+
# @return [String]
|
|
77
|
+
def inspect
|
|
78
|
+
"#<WOTS::PrivateKey param=#{param.name} w=#{param.w} keys=[REDACTED]>"
|
|
79
|
+
end
|
|
68
80
|
end
|
|
69
81
|
end
|
data/lib/wots/public_key.rb
CHANGED
|
@@ -16,56 +16,71 @@ 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
|
-
# @param [String] pub_seed
|
|
25
|
-
|
|
24
|
+
# @param [String] pub_seed Hex string of +param.n+ bytes.
|
|
25
|
+
# @param [WOTS::Address] addr The address passed to PrivateKey.from_seed.
|
|
26
|
+
# @raise ArgumentError
|
|
27
|
+
def self.from_private_key(private_key, pub_seed, addr: Address.new)
|
|
26
28
|
raise ArgumentError, 'private_key must be WOTS::PrivateKey.' unless private_key.is_a?(WOTS::PrivateKey)
|
|
27
29
|
param = private_key.param
|
|
28
30
|
raise ArgumentError, 'pub_seed must be hex string.' unless hex_string?(pub_seed)
|
|
29
31
|
raise ArgumentError, "pub_seed must be #{param.n} bytes." unless hex_to_bin(pub_seed).bytesize == param.n
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
param.chain(sk, 0, param.w - 1, pub_seed, addr)
|
|
35
|
-
end
|
|
36
|
-
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)
|
|
37
36
|
end
|
|
38
37
|
|
|
39
38
|
# Generate public key from +signature+.
|
|
40
39
|
# @param [WOTS::Signature] signature
|
|
41
|
-
# @param [String] pub_seed
|
|
42
|
-
# @param [String] message
|
|
40
|
+
# @param [String] pub_seed Hex string of +param.n+ bytes.
|
|
41
|
+
# @param [String] message Hex string of +param.n+ bytes.
|
|
42
|
+
# @param [WOTS::Address] addr The address passed to PrivateKey.from_seed.
|
|
43
43
|
# @return [WOTS::PublicKey]
|
|
44
44
|
# @raise ArgumentError
|
|
45
|
-
def self.from_signature(signature, pub_seed, message)
|
|
45
|
+
def self.from_signature(signature, pub_seed, message, addr: Address.new)
|
|
46
|
+
raise ArgumentError, 'signature must be WOTS::Signature.' unless signature.is_a?(WOTS::Signature)
|
|
46
47
|
param = signature.param
|
|
47
48
|
raise ArgumentError, 'pub_seed must be hex string.' unless hex_string?(pub_seed)
|
|
48
49
|
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
|
|
50
|
+
raise ArgumentError, "message must be hex string." unless hex_string?(message)
|
|
50
51
|
raise ArgumentError, "message must be #{param.n} bytes." unless hex_to_bin(message).bytesize == param.n
|
|
51
52
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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) }
|
|
55
56
|
|
|
56
|
-
|
|
57
|
+
PublicKey.new(param, keys)
|
|
58
|
+
end
|
|
57
59
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
62
72
|
|
|
63
|
-
PublicKey.
|
|
73
|
+
PublicKey.from_signature(signature, pub_seed, message, addr: addr) == self
|
|
64
74
|
end
|
|
65
75
|
|
|
66
76
|
def ==(other)
|
|
67
77
|
return false unless other.is_a?(PublicKey)
|
|
68
78
|
param == other.param && keys == other.keys
|
|
69
79
|
end
|
|
80
|
+
alias eql? ==
|
|
81
|
+
|
|
82
|
+
def hash
|
|
83
|
+
[self.class, param.name, param.w, keys].hash
|
|
84
|
+
end
|
|
70
85
|
end
|
|
71
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
|
@@ -2,31 +2,45 @@ 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. A string in an
|
|
7
|
+
# encoding that is not ASCII compatible (e.g. UTF-16) is not a hex string either.
|
|
5
8
|
# @param [String] data
|
|
6
9
|
# @return [Boolean]
|
|
7
10
|
# @raise [ArgumentError]
|
|
8
11
|
def hex_string?(data)
|
|
9
12
|
raise ArgumentError, 'data must be string' unless data.is_a?(String)
|
|
10
|
-
data.
|
|
13
|
+
return false unless data.encoding.ascii_compatible?
|
|
14
|
+
|
|
15
|
+
data.length.even? && data.match?(/\A[0-9a-fA-F]+\z/)
|
|
11
16
|
end
|
|
12
17
|
|
|
13
18
|
# Convert hex string +data+ to binary.
|
|
14
|
-
# @param [String] data
|
|
15
|
-
# @return [String]
|
|
16
|
-
# @raise [ArgumentError]
|
|
19
|
+
# @param [String] data Hex string.
|
|
20
|
+
# @return [String] Binary string.
|
|
21
|
+
# @raise [ArgumentError] If +data+ is not a hex string.
|
|
17
22
|
def hex_to_bin(data)
|
|
18
|
-
raise ArgumentError, 'data must be string' unless
|
|
19
|
-
|
|
23
|
+
raise ArgumentError, 'data must be hex string' unless hex_string?(data)
|
|
24
|
+
[data].pack('H*')
|
|
25
|
+
end
|
|
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
|
|
20
34
|
end
|
|
21
35
|
|
|
22
36
|
# Convert binary string +data+ to hex string.
|
|
23
|
-
# @param [String] data
|
|
24
|
-
# @return [String]
|
|
37
|
+
# @param [String] data Binary string.
|
|
38
|
+
# @return [String] Hex string.
|
|
25
39
|
# @raise [ArgumentError]
|
|
26
40
|
def bin_to_hex(data)
|
|
27
41
|
raise ArgumentError, 'data must be string' unless data.is_a?(String)
|
|
28
|
-
|
|
42
|
+
data.unpack1('H*')
|
|
29
43
|
end
|
|
30
44
|
|
|
31
45
|
end
|
|
32
|
-
end
|
|
46
|
+
end
|
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
|