noise-ruby 0.14.0 → 0.15.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/.dockerignore +1 -0
- data/.github/workflows/ruby.yml +39 -0
- data/README.md +150 -2
- data/Rakefile +12 -0
- data/lib/noise/connection/base.rb +188 -24
- data/lib/noise/connection/initiator.rb +6 -3
- data/lib/noise/connection/responder.rb +6 -3
- data/lib/noise/exceptions/handshake_already_finished_error.rb +9 -0
- data/lib/noise/exceptions/handshake_not_finished_error.rb +9 -0
- data/lib/noise/exceptions/handshake_not_started_error.rb +9 -0
- data/lib/noise/exceptions/handshake_turn_error.rb +11 -0
- data/lib/noise/exceptions/read_timeout_error.rb +9 -0
- data/lib/noise/exceptions/truncated_message_error.rb +9 -0
- data/lib/noise/exceptions.rb +6 -0
- data/lib/noise/pattern.rb +10 -8
- data/lib/noise/protocol.rb +43 -19
- data/lib/noise/protocol_name.rb +78 -0
- data/lib/noise/transport/bolt8.rb +183 -0
- data/lib/noise/transport/framed.rb +98 -0
- data/lib/noise/transport/stream.rb +130 -0
- data/lib/noise/transport.rb +14 -0
- data/lib/noise/version.rb +1 -1
- data/lib/noise.rb +5 -0
- data/noise.gemspec +3 -1
- metadata +12 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 92da6ad3c1e14197a093a54fd4e70f26cfa6cb15aec9d7fe6686b664d79235b2
|
|
4
|
+
data.tar.gz: 2837519b1708e55d0a0a052c11c9fd05da2fef1c1a4d881624602070fd07a579
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a36f16cb90bcb3ac4979031744dbd0d2de4e324e14209bc8ae4b073e92b5ee789c0b898ec641a6a92c59c7e07e4f7e4169140b0499e23695e11a31ea51ed5874
|
|
7
|
+
data.tar.gz: c12c1216154ce5bd8f37e7e6bb56a62cca0a5da7cfacc7bf2b8d2ed1014bd25a487450e62850092a1bee0f1847f2554f1f3bac5c8b216220eeea3c593cdc37c7
|
data/.dockerignore
CHANGED
data/.github/workflows/ruby.yml
CHANGED
|
@@ -53,3 +53,42 @@ jobs:
|
|
|
53
53
|
- name: Run tests
|
|
54
54
|
run: bundle exec rspec spec
|
|
55
55
|
|
|
56
|
+
interop:
|
|
57
|
+
runs-on: ubuntu-latest
|
|
58
|
+
# The suite finishes in seconds. Anything longer means two implementations are waiting on each
|
|
59
|
+
# other, and a job that hangs until the six hour default helps nobody.
|
|
60
|
+
timeout-minutes: 15
|
|
61
|
+
steps:
|
|
62
|
+
- name: Checkout code
|
|
63
|
+
uses: actions/checkout@v4
|
|
64
|
+
|
|
65
|
+
- name: Set up Ruby
|
|
66
|
+
uses: ruby/setup-ruby@v1
|
|
67
|
+
with:
|
|
68
|
+
ruby-version: '4.0'
|
|
69
|
+
bundler-cache: true
|
|
70
|
+
|
|
71
|
+
- name: Set up Rust
|
|
72
|
+
id: rust
|
|
73
|
+
uses: dtolnay/rust-toolchain@stable
|
|
74
|
+
|
|
75
|
+
# interop/target holds compiler fingerprints, so the toolchain belongs in the key: a new
|
|
76
|
+
# stable rustc has to start from a cache of its own rather than rebuild under an old one.
|
|
77
|
+
- name: Cache the interoperability harness build
|
|
78
|
+
uses: actions/cache@v4
|
|
79
|
+
with:
|
|
80
|
+
path: |
|
|
81
|
+
~/.cargo/registry
|
|
82
|
+
~/.cargo/git
|
|
83
|
+
interop/target
|
|
84
|
+
key: interop-${{ runner.os }}-${{ steps.rust.outputs.cachekey }}-${{ hashFiles('interop/Cargo.lock') }}
|
|
85
|
+
restore-keys: interop-${{ runner.os }}-${{ steps.rust.outputs.cachekey }}-
|
|
86
|
+
|
|
87
|
+
- name: Build the interoperability harness
|
|
88
|
+
run: cargo build --release --locked
|
|
89
|
+
working-directory: interop
|
|
90
|
+
|
|
91
|
+
# A real handshake against snow, the Rust implementation. See interop/README.md.
|
|
92
|
+
- name: Run the interoperability suite
|
|
93
|
+
run: bundle exec rake interop
|
|
94
|
+
|
data/README.md
CHANGED
|
@@ -96,7 +96,7 @@ it.
|
|
|
96
96
|
```
|
|
97
97
|
initiator = Noise::Connection::Initiator.new("Noise_NN_25519_ChaChaPoly_BLAKE2b")
|
|
98
98
|
initiator.prologue = "test" # => "test"
|
|
99
|
-
initiator.start_handshake
|
|
99
|
+
initiator.start_handshake
|
|
100
100
|
cipher = initiator.write_message("") # => "\xB6\xF7gmxi\xAB\xBCY|t\xF0\x9D\x01A\ad\x92\xBBvp\x80ZNU\f=\x83\x81^\xFD\x15"
|
|
101
101
|
```
|
|
102
102
|
|
|
@@ -110,7 +110,7 @@ The responder responds messages to the initiator.
|
|
|
110
110
|
```
|
|
111
111
|
responder = Noise::Connection::Responder.new("Noise_NN_25519_ChaChaPoly_BLAKE2b")
|
|
112
112
|
responder.prologue = "test" # => "test"
|
|
113
|
-
responder.start_handshake
|
|
113
|
+
responder.start_handshake
|
|
114
114
|
plain = responder.read_message(cipher) # => ""
|
|
115
115
|
cipher = responder.write_message("") # => "\v\xD9\x97'\xC0\xB1\xC9\xFFD\x8C\x7F\x18L\xB0\xF2\x14\xB0\x11\xC0\x90\xAAZ\xE1\x03\x17z)\xB81/5L\x16\xE3\xD1\xBE<{\xB8\xBB\xD6\xF1\x00\x10]\x99=\xD7"
|
|
116
116
|
```
|
|
@@ -161,6 +161,154 @@ initiator.rekey_encryption
|
|
|
161
161
|
responder.rekey_decryption
|
|
162
162
|
```
|
|
163
163
|
|
|
164
|
+
### Thread safety
|
|
165
|
+
|
|
166
|
+
One connection belongs to one thread. A `Noise::Connection` holds its handshake state and its
|
|
167
|
+
transport nonces in plain instance variables and never locks, so two threads that share one
|
|
168
|
+
connection can encrypt two different plaintexts under the same nonce. That is not a small loss for
|
|
169
|
+
either cipher this gem implements: reusing a nonce breaks the confidentiality of both messages and
|
|
170
|
+
lets an attacker forge further ones.
|
|
171
|
+
|
|
172
|
+
If an application has to reach one connection from more than one thread, it is responsible for
|
|
173
|
+
serialising every call to it.
|
|
174
|
+
|
|
175
|
+
This is deliberate rather than an omission. A lock inside the connection would make each call
|
|
176
|
+
atomic without making concurrent use correct, because the transport nonce numbers the messages of
|
|
177
|
+
a direction: two threads that both encrypt still produce a stream in an order the receiver cannot
|
|
178
|
+
reconstruct. Only the application knows which message comes first, and deciding that order is
|
|
179
|
+
itself the serialisation the lock cannot supply.
|
|
180
|
+
|
|
181
|
+
Receiving is no easier. Decrypting a message that arrived out of order is a `decryption_nonce=`
|
|
182
|
+
call followed by a `decrypt` call, and no lock taken one call at a time holds those two together.
|
|
183
|
+
|
|
184
|
+
### Half-duplex mode
|
|
185
|
+
|
|
186
|
+
Section 11.5 of the Noise specification describes protocols in which the two parties strictly take
|
|
187
|
+
turns. Such a protocol may keep a single `CipherState` for both directions instead of one per
|
|
188
|
+
direction, which halves what each party has to store after the handshake. Pass `half_duplex: true`
|
|
189
|
+
to both parties to run that way.
|
|
190
|
+
|
|
191
|
+
```
|
|
192
|
+
initiator = Noise::Connection::Initiator.new("Noise_NN_25519_ChaChaPoly_SHA256", half_duplex: true)
|
|
193
|
+
responder = Noise::Connection::Responder.new("Noise_NN_25519_ChaChaPoly_SHA256", half_duplex: true)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Because there is one `CipherState`, the methods that name a direction all reach it:
|
|
197
|
+
`encryption_nonce` and `decryption_nonce` report the same count, setting either sets both, and
|
|
198
|
+
`rekey_encryption` and `rekey_decryption` replace the same key.
|
|
199
|
+
|
|
200
|
+
**The two parties must strictly alternate their transport messages.** They share one nonce, so a
|
|
201
|
+
message in either direction advances the count both of them keep. If both parties encrypt before
|
|
202
|
+
either decrypts, the two messages go out under the same nonce, which breaks the confidentiality of
|
|
203
|
+
both and lets an attacker forge further ones. Nothing in this gem can detect that: it is the
|
|
204
|
+
application protocol that has to guarantee the turn taking.
|
|
205
|
+
|
|
206
|
+
Use it only when the protocol you are implementing calls for it. It is off by default, and a
|
|
207
|
+
one-way pattern refuses it, because such a pattern has no messages to alternate.
|
|
208
|
+
|
|
209
|
+
### Framing a connection for a socket
|
|
210
|
+
|
|
211
|
+
`encrypt` and `decrypt` work on whole messages, which is the shape the Noise specification
|
|
212
|
+
describes but not the shape a socket has. `Noise::Transport::Framed` puts a finished connection on
|
|
213
|
+
top of an `IO` by writing each message preceded by its length, so that the reader knows how many
|
|
214
|
+
bytes to take.
|
|
215
|
+
|
|
216
|
+
```
|
|
217
|
+
transport = Noise::Transport::Framed.new(connection, socket)
|
|
218
|
+
|
|
219
|
+
transport.write("a message")
|
|
220
|
+
message = transport.read
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
`read` returns one message, waiting until all of it has arrived however many pieces it comes in.
|
|
224
|
+
It answers `nil` when the stream ends between messages, which is how the other party closes
|
|
225
|
+
without cutting one in half, and raises `TruncatedMessageError` when the stream ends part way
|
|
226
|
+
through one. `write` returns once the whole frame has gone out.
|
|
227
|
+
|
|
228
|
+
Every failure ends the transport, so build a new connection rather than reading again. A
|
|
229
|
+
`DecryptError` means the frame was not written by the party this connection shares a key with, or
|
|
230
|
+
not in the order it claims. A `TruncatedMessageError` or a `ReadTimeoutError` leaves the bytes
|
|
231
|
+
already taken out of the stream and nowhere to put them, so the next read would take the middle of
|
|
232
|
+
a message for a length. Errors the socket itself raises, such as `Errno::ECONNRESET`, come through
|
|
233
|
+
as they are.
|
|
234
|
+
|
|
235
|
+
Pass `read_timeout:` in seconds to give up on a message that stops arriving:
|
|
236
|
+
|
|
237
|
+
```
|
|
238
|
+
transport = Noise::Transport::Framed.new(connection, socket, read_timeout: 30)
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
The timeout applies to each wait for more bytes rather than to the message as a whole, so a peer
|
|
242
|
+
that sends a byte at a time holds the read open without ever tripping it. It needs a stream that
|
|
243
|
+
answers `wait_readable`, which a socket does and a `StringIO` does not; asking for a timeout on
|
|
244
|
+
one that cannot honour it raises `ArgumentError` rather than dropping it silently.
|
|
245
|
+
|
|
246
|
+
The length goes out in the clear, as two big-endian bytes. Two are enough because a Noise message
|
|
247
|
+
may not exceed 65535 bytes, and a payload is shorter than that by its authentication tag. The
|
|
248
|
+
length being in the clear means this framing hides nothing about how long each message is; a
|
|
249
|
+
protocol that has to hide its message sizes pads them, or encrypts the length as BOLT #8 does.
|
|
250
|
+
|
|
251
|
+
Once a connection is framed, stop calling its `encrypt` and `decrypt` directly. A message that
|
|
252
|
+
goes out unframed leaves the reader taking the next message's bytes for a length.
|
|
253
|
+
|
|
254
|
+
### Lightning Network (BOLT #8)
|
|
255
|
+
|
|
256
|
+
BOLT #8 is the transport the Lightning Network runs on: a `Noise_XK_secp256k1_ChaChaPoly_SHA256`
|
|
257
|
+
handshake, then a byte stream in which every message is preceded by its own encrypted length.
|
|
258
|
+
`Noise::Transport::Bolt8` owns that framing, so an application only has to run the handshake and
|
|
259
|
+
hand over the finished connection.
|
|
260
|
+
|
|
261
|
+
```
|
|
262
|
+
name = Noise::Transport::Bolt8::PROTOCOL_NAME
|
|
263
|
+
|
|
264
|
+
initiator = Noise::Connection::Initiator.new(name, keypairs: { s: local_static, rs: node_id })
|
|
265
|
+
initiator.prologue = Noise::Transport::Bolt8::PROLOGUE
|
|
266
|
+
initiator.start_handshake
|
|
267
|
+
# ... exchange the three handshake messages over the socket ...
|
|
268
|
+
|
|
269
|
+
transport = Noise::Transport::Bolt8.new(initiator, socket)
|
|
270
|
+
transport.write("a lightning message")
|
|
271
|
+
message = transport.read
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
It reads and writes exactly as `Framed` does above, `read_timeout:` and the `nil` that means the
|
|
275
|
+
other party closed between messages included. What differs is the framing: the length is encrypted as a
|
|
276
|
+
Noise message of its own rather than sent in the clear, so a watcher cannot tell how long each
|
|
277
|
+
message is. `DecryptError` means the same thing it does there, and BOLT #8 requires the connection
|
|
278
|
+
to be closed when it happens.
|
|
279
|
+
|
|
280
|
+
Each direction replaces its key with `HKDF(ck, k)` once its nonce reaches 1000, which is every 500
|
|
281
|
+
messages because each message is encrypted twice. This is not the `rekey_encryption` of the Noise
|
|
282
|
+
specification: it draws on the chaining key the handshake ended with, so a key stolen now says
|
|
283
|
+
nothing about the keys that direction used before it. The transport does it on its own; nothing
|
|
284
|
+
has to be called.
|
|
285
|
+
|
|
286
|
+
The transport takes over the connection's transport phase: it holds the very `CipherState`s the
|
|
287
|
+
connection does. Once a connection is wrapped, stop calling its `encrypt`, `decrypt`, nonce
|
|
288
|
+
accessors and rekey methods, because each of them moves the same key and nonce and the peer has no
|
|
289
|
+
way to learn that they did. A half-duplex connection is refused outright, since BOLT #8 gives each
|
|
290
|
+
direction a key of its own.
|
|
291
|
+
|
|
292
|
+
Nothing else in the gem loads this. An application that does not speak Lightning never names
|
|
293
|
+
`Noise::Transport::Bolt8`, and never pays for it.
|
|
294
|
+
|
|
295
|
+
## Interoperability
|
|
296
|
+
|
|
297
|
+
The suite checks this gem against the official test vectors in `spec/vectors/`, which are replayed
|
|
298
|
+
transcripts. `interop/` runs the other kind of test: a real handshake, and then a real transport
|
|
299
|
+
exchange, between this gem and [snow](https://github.com/mcginty/snow), the Rust implementation of
|
|
300
|
+
the framework. It covers eighteen patterns against five cipher and hash suites, in both
|
|
301
|
+
directions.
|
|
302
|
+
|
|
303
|
+
It needs a Rust toolchain, so it is not part of `bundle exec rspec`:
|
|
304
|
+
|
|
305
|
+
```
|
|
306
|
+
cd interop && cargo build --release && cd ..
|
|
307
|
+
bundle exec rake interop
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
See [interop/README.md](interop/README.md) for what it covers and what it does not.
|
|
311
|
+
|
|
164
312
|
## Development
|
|
165
313
|
|
|
166
314
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/Rakefile
CHANGED
|
@@ -5,4 +5,16 @@ require 'rspec/core/rake_task'
|
|
|
5
5
|
|
|
6
6
|
RSpec::Core::RakeTask.new(:spec)
|
|
7
7
|
|
|
8
|
+
desc 'Build the harness the interoperability suite runs against'
|
|
9
|
+
task :interop_harness do
|
|
10
|
+
Dir.chdir('interop') { sh 'cargo build --release --locked' }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
desc 'Run the interoperability suite against the snow reference implementation'
|
|
14
|
+
RSpec::Core::RakeTask.new(interop: :interop_harness) do |task|
|
|
15
|
+
task.pattern = 'interop/*_spec.rb'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# interop is deliberately left out: it needs a Rust toolchain, and the harness built from
|
|
19
|
+
# interop/. See interop/README.md.
|
|
8
20
|
task default: :spec
|
|
@@ -2,6 +2,39 @@
|
|
|
2
2
|
|
|
3
3
|
module Noise
|
|
4
4
|
module Connection
|
|
5
|
+
# A connection moves through four states, and every public operation is legal in some of them
|
|
6
|
+
# and not in others:
|
|
7
|
+
#
|
|
8
|
+
# :created -- start_handshake
|
|
9
|
+
# :handshake_write -- write_message, fallback
|
|
10
|
+
# :handshake_read -- read_message, fallback
|
|
11
|
+
# :transport -- encrypt, decrypt, rekey_*, the nonce accessors
|
|
12
|
+
#
|
|
13
|
+
# start_handshake moves :created to the state this party starts the pattern in, which is
|
|
14
|
+
# :handshake_write for the initiator and :handshake_read for the responder. Writing or reading
|
|
15
|
+
# a handshake message passes the turn to the other party, and the message that completes the
|
|
16
|
+
# pattern moves the connection to :transport.
|
|
17
|
+
#
|
|
18
|
+
# == Threads
|
|
19
|
+
#
|
|
20
|
+
# One connection belongs to one thread. Nothing here is synchronised: the state above is a
|
|
21
|
+
# plain instance variable, and #encrypt and #decrypt advance the transport nonce with a plain
|
|
22
|
+
# +=, so two threads that share a connection can encrypt two different plaintexts under the
|
|
23
|
+
# same nonce. Neither ChaChaPoly nor AESGCM degrades gracefully when that happens: reusing a
|
|
24
|
+
# nonce breaks the confidentiality of both messages and lets an attacker forge further ones.
|
|
25
|
+
#
|
|
26
|
+
# A caller that has to reach one connection from more than one thread is responsible for
|
|
27
|
+
# serialising every call to it.
|
|
28
|
+
#
|
|
29
|
+
# This class deliberately does not lock. A Mutex would make each call atomic without making
|
|
30
|
+
# concurrent use correct: the transport nonce numbers the messages of a direction, so two
|
|
31
|
+
# threads that both encrypt still hand the transport layer a stream whose order the receiver
|
|
32
|
+
# cannot reconstruct. Only the application knows which message is meant to be first, and once
|
|
33
|
+
# it has said so, its own serialisation has already done what the lock would have done.
|
|
34
|
+
#
|
|
35
|
+
# The other direction is no better. Decrypting a message that arrived out of order is a
|
|
36
|
+
# #decryption_nonce= call followed by a #decrypt call, a sequence no per-call lock can hold
|
|
37
|
+
# together.
|
|
5
38
|
class Base
|
|
6
39
|
# The Noise spec caps a handshake or transport message at 65535 bytes. A transport message is
|
|
7
40
|
# the ciphertext, so the plaintext a caller may hand to encrypt is shorter by the
|
|
@@ -9,7 +42,29 @@ module Noise
|
|
|
9
42
|
MAX_MESSAGE_LENGTH = 65_535
|
|
10
43
|
MAX_PLAINTEXT_LENGTH = MAX_MESSAGE_LENGTH - Noise::State::CipherState::TAG_LENGTH
|
|
11
44
|
|
|
12
|
-
|
|
45
|
+
# The states in which the connection is running a handshake, and what it expects next in
|
|
46
|
+
# each of them. The message explains a HandshakeTurnError.
|
|
47
|
+
TURN_MESSAGES = {
|
|
48
|
+
handshake_write: 'This party writes the next handshake message; it cannot read one yet.',
|
|
49
|
+
handshake_read: 'This party reads the next handshake message; it cannot write one yet.'
|
|
50
|
+
}.freeze
|
|
51
|
+
private_constant :TURN_MESSAGES
|
|
52
|
+
|
|
53
|
+
# The states in which a handshake is running, which are the ones #fallback may be called in.
|
|
54
|
+
HANDSHAKE_STATES = TURN_MESSAGES.keys.freeze
|
|
55
|
+
private_constant :HANDSHAKE_STATES
|
|
56
|
+
|
|
57
|
+
# @return [Symbol] the current state, one of :created, :handshake_write, :handshake_read and
|
|
58
|
+
# :transport.
|
|
59
|
+
attr_reader :state
|
|
60
|
+
|
|
61
|
+
# @return [String, nil] the chaining key the handshake ended with, once it has. Split()
|
|
62
|
+
# derives the transport keys from it and leaves it as it is, so a transport layer that
|
|
63
|
+
# derives its own keys from it later — BOLT #8 rotates its keys this way — needs it after
|
|
64
|
+
# the handshake. It is as secret as the transport keys it produced.
|
|
65
|
+
attr_reader :chaining_key
|
|
66
|
+
|
|
67
|
+
attr_reader :protocol, :handshake_hash, :handshake_state,
|
|
13
68
|
:cipher_state_encrypt, :cipher_state_decrypt, :cipher_state_handshake, :s, :rs
|
|
14
69
|
attr_accessor :psks, :prologue
|
|
15
70
|
|
|
@@ -23,37 +78,87 @@ module Noise
|
|
|
23
78
|
# the keypair given here instead of generating a fresh one, and an ephemeral key reused
|
|
24
79
|
# across handshakes gives up the forward secrecy every pattern depends on. The handshake
|
|
25
80
|
# still succeeds, so nothing reports the loss.
|
|
26
|
-
|
|
81
|
+
# @param [Boolean] half_duplex whether to run the transport phase in half-duplex mode, in
|
|
82
|
+
# which one CipherState encrypts both directions. See #half_duplex? for what that costs.
|
|
83
|
+
# @raise [Noise::Exceptions::NoiseValidationError] if half_duplex is asked for on a one-way
|
|
84
|
+
# pattern, which has no messages to alternate.
|
|
85
|
+
def initialize(name, keypairs: { s: nil, e: nil, rs: nil, re: nil }, half_duplex: false)
|
|
27
86
|
@protocol = Protocol.create(name)
|
|
87
|
+
@half_duplex = half_duplex
|
|
88
|
+
validate_half_duplex!
|
|
28
89
|
|
|
29
90
|
# parameter keypairs[:e] and keypairs[:s] are strings, so should convert Noise::Key object.
|
|
30
91
|
@local_keypairs = {}
|
|
31
92
|
@local_keypairs[:e] = @protocol.dh_fn.class.from_private(keypairs[:e]) if keypairs[:e]
|
|
32
93
|
@local_keypairs[:s] = @protocol.dh_fn.class.from_private(keypairs[:s]) if keypairs[:s]
|
|
33
94
|
@remote_keys = { rs: keypairs[:rs], re: keypairs[:re] }
|
|
34
|
-
@
|
|
35
|
-
@handshake_finished = false
|
|
36
|
-
initialize_next_message
|
|
95
|
+
@state = :created
|
|
37
96
|
end
|
|
38
97
|
|
|
98
|
+
# Whether the transport phase uses one CipherState for both directions, which is the
|
|
99
|
+
# half-duplex mode of section 11.5 of the Noise specification. Both parties keep the first
|
|
100
|
+
# CipherState that Split() returns and drop the second.
|
|
101
|
+
#
|
|
102
|
+
# It is only safe when the two parties strictly alternate their transport messages. They
|
|
103
|
+
# share one nonce, so if both encrypt before either decrypts, two different plaintexts go
|
|
104
|
+
# out under the same nonce, which breaks the confidentiality of both and lets an attacker
|
|
105
|
+
# forge further ones. Nothing here can check that the application alternates; that is the
|
|
106
|
+
# application protocol's guarantee to make.
|
|
107
|
+
#
|
|
108
|
+
# Because there is one CipherState, the accessors that name a direction all reach it:
|
|
109
|
+
# #encryption_nonce and #decryption_nonce report the same count, setting either sets both,
|
|
110
|
+
# and #rekey_encryption and #rekey_decryption replace the same key.
|
|
111
|
+
#
|
|
112
|
+
# @return [Boolean]
|
|
113
|
+
def half_duplex?
|
|
114
|
+
@half_duplex
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# @return [Boolean] true once #start_handshake has been called.
|
|
118
|
+
def handshake_started?
|
|
119
|
+
@state != :created
|
|
120
|
+
end
|
|
121
|
+
alias handshake_started handshake_started?
|
|
122
|
+
|
|
123
|
+
# @return [Boolean] true once the last handshake message has been written or read, which is
|
|
124
|
+
# when the transport operations become available.
|
|
125
|
+
def handshake_finished?
|
|
126
|
+
@state == :transport
|
|
127
|
+
end
|
|
128
|
+
alias handshake_finished handshake_finished?
|
|
129
|
+
|
|
130
|
+
# Starts the handshake this connection was created for. It is legal once: a connection that
|
|
131
|
+
# already has a handshake, running or finished, has keys and a transcript that restarting
|
|
132
|
+
# would silently throw away. #fallback is the supported way to begin a second handshake.
|
|
39
133
|
def start_handshake
|
|
134
|
+
ensure_state!(:created)
|
|
40
135
|
validate
|
|
41
136
|
initialise_handshake_state
|
|
42
|
-
@
|
|
137
|
+
@state = initial_turn
|
|
138
|
+
nil
|
|
43
139
|
end
|
|
44
140
|
|
|
45
141
|
# Restarts the handshake with a fallback pattern, carrying over the keys of the aborted one.
|
|
46
142
|
#
|
|
47
143
|
# The roles swap here: the party that wrote the aborted message now reads, and the one that
|
|
48
|
-
# failed to read it now writes. Both sides are already
|
|
49
|
-
#
|
|
144
|
+
# failed to read it now writes. Both sides are already on that turn, so the current turn is
|
|
145
|
+
# carried over rather than reset to the one this party started the aborted pattern on.
|
|
50
146
|
def fallback(fallback_name)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
147
|
+
ensure_state!(*HANDSHAKE_STATES)
|
|
148
|
+
|
|
149
|
+
turn = @state
|
|
150
|
+
protocol = Protocol.create(fallback_name)
|
|
151
|
+
validate_half_duplex!(protocol)
|
|
152
|
+
@protocol = protocol
|
|
54
153
|
@local_keypairs = { e: @handshake_state.e, s: @handshake_state.s }
|
|
55
154
|
@remote_keys = { re: @handshake_state.re, rs: @handshake_state.rs }
|
|
155
|
+
# Everything that can fail has run by now, so a bad fallback name leaves the connection on
|
|
156
|
+
# the handshake it was already running. start_handshake accepts a connection in :created
|
|
157
|
+
# alone, so the aborted handshake is cleared here and the turn restored after it.
|
|
158
|
+
@state = :created
|
|
56
159
|
start_handshake
|
|
160
|
+
@state = turn
|
|
161
|
+
nil
|
|
57
162
|
end
|
|
58
163
|
|
|
59
164
|
def initialise_handshake_state
|
|
@@ -69,10 +174,7 @@ module Noise
|
|
|
69
174
|
end
|
|
70
175
|
|
|
71
176
|
def write_message(payload = '')
|
|
72
|
-
|
|
73
|
-
raise Noise::Exceptions::NoiseHandshakeError unless @handshake_started
|
|
74
|
-
raise Noise::Exceptions::NoiseHandshakeError if @next_message != :write
|
|
75
|
-
raise Noise::Exceptions::NoiseHandshakeError if @handshake_finished
|
|
177
|
+
ensure_state!(:handshake_write)
|
|
76
178
|
|
|
77
179
|
length = @handshake_state.expected_message_length(payload.bytesize)
|
|
78
180
|
if length > MAX_MESSAGE_LENGTH
|
|
@@ -80,26 +182,27 @@ module Noise
|
|
|
80
182
|
"Message would be #{length} bytes, which exceeds the maximum of #{MAX_MESSAGE_LENGTH}."
|
|
81
183
|
end
|
|
82
184
|
|
|
83
|
-
|
|
185
|
+
# The turn passes before the message is built, so that a message this party failed to
|
|
186
|
+
# build still leaves the connection waiting for the other party, as fallback expects.
|
|
187
|
+
@state = :handshake_read
|
|
84
188
|
buffer = +''
|
|
85
|
-
@
|
|
189
|
+
@state = :transport if @handshake_state.write_message(payload, buffer)
|
|
86
190
|
buffer
|
|
87
191
|
end
|
|
88
192
|
|
|
89
193
|
def read_message(data)
|
|
90
|
-
|
|
91
|
-
raise Noise::Exceptions::NoiseHandshakeError unless @handshake_started
|
|
92
|
-
raise Noise::Exceptions::NoiseHandshakeError if @next_message != :read
|
|
93
|
-
raise Noise::Exceptions::NoiseHandshakeError if @handshake_finished
|
|
194
|
+
ensure_state!(:handshake_read)
|
|
94
195
|
|
|
95
196
|
if data.bytesize > MAX_MESSAGE_LENGTH
|
|
96
197
|
raise Noise::Exceptions::MessageTooLongError,
|
|
97
198
|
"Message is #{data.bytesize} bytes, which exceeds the maximum of #{MAX_MESSAGE_LENGTH}."
|
|
98
199
|
end
|
|
99
200
|
|
|
100
|
-
|
|
201
|
+
# See #write_message: the turn passes before the message is read, so that a message that
|
|
202
|
+
# fails to decrypt leaves this party ready to write the fallback handshake message.
|
|
203
|
+
@state = :handshake_write
|
|
101
204
|
buffer = +''
|
|
102
|
-
@
|
|
205
|
+
@state = :transport if @handshake_state.read_message(data, buffer)
|
|
103
206
|
buffer
|
|
104
207
|
end
|
|
105
208
|
|
|
@@ -197,6 +300,7 @@ module Noise
|
|
|
197
300
|
|
|
198
301
|
def handshake_done(_c1, _c2)
|
|
199
302
|
@handshake_hash = @symmetric_state.handshake_hash
|
|
303
|
+
@chaining_key = @symmetric_state.ck
|
|
200
304
|
@s = @handshake_state.s
|
|
201
305
|
@rs = @handshake_state.rs
|
|
202
306
|
@handshake_state = nil
|
|
@@ -206,6 +310,36 @@ module Noise
|
|
|
206
310
|
|
|
207
311
|
private
|
|
208
312
|
|
|
313
|
+
# A one-way pattern gives the responder no way to reply, so its parties never alternate and
|
|
314
|
+
# half-duplex has nothing to share a CipherState between. Sharing one anyway would hand the
|
|
315
|
+
# responder an encryption key the pattern is meant to deny it.
|
|
316
|
+
#
|
|
317
|
+
# @param [Noise::Protocol] protocol the protocol to check, which is the one being fallen back
|
|
318
|
+
# to when #fallback asks.
|
|
319
|
+
# @raise [Noise::Exceptions::NoiseValidationError]
|
|
320
|
+
def validate_half_duplex!(protocol = @protocol)
|
|
321
|
+
return unless @half_duplex && protocol.pattern.one_way?
|
|
322
|
+
|
|
323
|
+
raise Noise::Exceptions::NoiseValidationError,
|
|
324
|
+
"#{protocol.name} is a one-way pattern, which cannot run half-duplex."
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# The CipherState for the direction this party did not assign c1 to, which is decryption for
|
|
328
|
+
# the initiator and encryption for the responder.
|
|
329
|
+
#
|
|
330
|
+
# Half-duplex answers c1 both ways, so that the two parties encrypt and decrypt under the
|
|
331
|
+
# same key and nonce as they take turns. A one-way pattern has no second direction at all,
|
|
332
|
+
# so it has none.
|
|
333
|
+
#
|
|
334
|
+
# @param [Noise::State::CipherState] c1 the first CipherState Split() returned.
|
|
335
|
+
# @param [Noise::State::CipherState] c2 the second one.
|
|
336
|
+
# @return [Noise::State::CipherState, nil]
|
|
337
|
+
def other_direction_cipher_state(c1, c2)
|
|
338
|
+
return c1 if half_duplex?
|
|
339
|
+
|
|
340
|
+
@protocol.pattern.one_way? ? nil : c2
|
|
341
|
+
end
|
|
342
|
+
|
|
209
343
|
# Returns the transport CipherState of the given direction.
|
|
210
344
|
#
|
|
211
345
|
# One-way patterns leave the direction the caller cannot use as nil, so the absence of a
|
|
@@ -213,13 +347,43 @@ module Noise
|
|
|
213
347
|
#
|
|
214
348
|
# @param [Symbol] direction :encrypt or :decrypt.
|
|
215
349
|
def transport_cipher_state(direction)
|
|
216
|
-
|
|
350
|
+
ensure_state!(:transport)
|
|
217
351
|
|
|
218
352
|
cipher_state = direction == :encrypt ? @cipher_state_encrypt : @cipher_state_decrypt
|
|
219
353
|
raise Noise::Exceptions::NoiseHandshakeError, "This party cannot #{direction} messages." unless cipher_state
|
|
220
354
|
|
|
221
355
|
cipher_state
|
|
222
356
|
end
|
|
357
|
+
|
|
358
|
+
# Raises unless the connection is in one of the states the calling operation is legal in.
|
|
359
|
+
# Every public operation goes through here, so a new one cannot forget its precondition.
|
|
360
|
+
#
|
|
361
|
+
# @param [Array<Symbol>] allowed the states the operation may be called in.
|
|
362
|
+
def ensure_state!(*allowed)
|
|
363
|
+
return if allowed.include?(@state)
|
|
364
|
+
|
|
365
|
+
raise state_error(allowed)
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# Builds the exception for an operation called in the wrong state. All four descend from
|
|
369
|
+
# NoiseHandshakeError, so a caller that does not care which one it is can rescue the parent.
|
|
370
|
+
#
|
|
371
|
+
# @param [Array<Symbol>] allowed the states the operation may be called in.
|
|
372
|
+
# @return [Noise::Exceptions::NoiseHandshakeError]
|
|
373
|
+
def state_error(allowed)
|
|
374
|
+
case @state
|
|
375
|
+
when :created
|
|
376
|
+
Noise::Exceptions::HandshakeNotStartedError.new('The handshake has not started. Call #start_handshake.')
|
|
377
|
+
when :transport
|
|
378
|
+
Noise::Exceptions::HandshakeAlreadyFinishedError.new('The handshake has already finished.')
|
|
379
|
+
else
|
|
380
|
+
if allowed.include?(:transport)
|
|
381
|
+
Noise::Exceptions::HandshakeNotFinishedError.new('The handshake has not finished.')
|
|
382
|
+
else
|
|
383
|
+
Noise::Exceptions::HandshakeTurnError.new(TURN_MESSAGES.fetch(@state))
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
end
|
|
223
387
|
end
|
|
224
388
|
end
|
|
225
389
|
end
|
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
module Noise
|
|
4
4
|
module Connection
|
|
5
5
|
class Initiator < Base
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
# The initiator writes the first handshake message of the pattern.
|
|
7
|
+
#
|
|
8
|
+
# @return [Symbol] the state start_handshake moves this party into.
|
|
9
|
+
def initial_turn
|
|
10
|
+
:handshake_write
|
|
8
11
|
end
|
|
9
12
|
|
|
10
13
|
def initiator?
|
|
@@ -14,7 +17,7 @@ module Noise
|
|
|
14
17
|
def handshake_done(c1, c2)
|
|
15
18
|
super
|
|
16
19
|
@cipher_state_encrypt = c1
|
|
17
|
-
@cipher_state_decrypt =
|
|
20
|
+
@cipher_state_decrypt = other_direction_cipher_state(c1, c2)
|
|
18
21
|
end
|
|
19
22
|
end
|
|
20
23
|
end
|
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
module Noise
|
|
4
4
|
module Connection
|
|
5
5
|
class Responder < Base
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
# The responder reads the first handshake message of the pattern.
|
|
7
|
+
#
|
|
8
|
+
# @return [Symbol] the state start_handshake moves this party into.
|
|
9
|
+
def initial_turn
|
|
10
|
+
:handshake_read
|
|
8
11
|
end
|
|
9
12
|
|
|
10
13
|
def initiator?
|
|
@@ -14,7 +17,7 @@ module Noise
|
|
|
14
17
|
def handshake_done(c1, c2)
|
|
15
18
|
super
|
|
16
19
|
@cipher_state_decrypt = c1
|
|
17
|
-
@cipher_state_encrypt =
|
|
20
|
+
@cipher_state_encrypt = other_direction_cipher_state(c1, c2)
|
|
18
21
|
end
|
|
19
22
|
end
|
|
20
23
|
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Noise
|
|
4
|
+
module Exceptions
|
|
5
|
+
# Raised when a handshake message is written where one has to be read, or read where one has to
|
|
6
|
+
# be written. A Noise handshake alternates between the two parties, so only one of the two is
|
|
7
|
+
# legal at any point.
|
|
8
|
+
class HandshakeTurnError < NoiseHandshakeError
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|