1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
|
//! A challenge handshake that proves, at the start of a connection, which key is at the
//! other end: for transports that don't authenticate their peers themselves (an onion
//! service learns nothing about who connects to it, `transport::tor`).
//!
//! Both sides send a HELLO: their public key and a fresh random nonce. Then each sends
//! a PROOF: its signature over a label, its role (dialer or listener), the connection's
//! context, and both HELLOs (its own first). Each checks the other's signature against
//! the key in the other's HELLO, and from then on knows that key is at the other end.
//!
//! * A recorded PROOF is useless elsewhere: it signs the other side's nonce, fresh for
//! each connection.
//! * The roles keep a side's own PROOF from being reflected back to it.
//! * The context binds the handshake to the connection as the dialer meant it: the
//! listener's address, as the dialer dialed it and as the listener knows itself. A
//! party in the middle that passes one side's handshake on to someone else fails
//! there: the context it was signed for is a different one.
//!
//! What it doesn't do: protect what follows. That is the connection's job, so the
//! handshake is only enough over one that is encrypted and tamper-proof end to end, and
//! authenticates the listener to the dialer (a Tor stream to an onion service). Over a
//! bare network (UDP, say), it would need key agreement and protected messages as well.
//!
//! The signing key is the node key (`crate::identity`); the label keeps these
//! signatures apart from the node key's others (WHO and ME).
use std::time::Duration;
use anyhow::{Result, anyhow, bail};
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use crate::identity::{self, NodeKey};
const LABEL: &[u8] = b"continuum/challenge/1";
/// The first byte of a HELLO: this handshake's version.
const VERSION: u8 = 1;
const HELLO_BYTES: usize = 1 + 32 + 32;
const PROOF_BYTES: usize = 64;
/// Which end of the connection a side is.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Role {
Dialer,
Listener,
}
impl Role {
fn byte(self) -> u8 {
match self {
Self::Dialer => 0,
Self::Listener => 1,
}
}
fn other(self) -> Self {
match self {
Self::Dialer => Self::Listener,
Self::Listener => Self::Dialer,
}
}
}
/// One side's HELLO.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct Hello {
pub(crate) key: [u8; 32],
pub(crate) nonce: [u8; 32],
}
impl Hello {
/// Ours, with a fresh nonce.
pub(crate) fn new(key: &NodeKey) -> Result<Self> {
let mut nonce = [0u8; 32];
getrandom::fill(&mut nonce).map_err(|error| anyhow!("No randomness: {error}"))?;
Ok(Self {
key: key.public(),
nonce,
})
}
pub(crate) fn to_bytes(self) -> [u8; HELLO_BYTES] {
let mut bytes = [0u8; HELLO_BYTES];
bytes[0] = VERSION;
bytes[1..33].copy_from_slice(&self.key);
bytes[33..].copy_from_slice(&self.nonce);
bytes
}
pub(crate) fn from_bytes(bytes: &[u8; HELLO_BYTES]) -> Result<Self> {
if bytes[0] != VERSION {
bail!(
"challenge handshake version {} (we speak {VERSION})",
bytes[0]
);
}
let mut hello = Self {
key: [0; 32],
nonce: [0; 32],
};
hello.key.copy_from_slice(&bytes[1..33]);
hello.nonce.copy_from_slice(&bytes[33..]);
Ok(hello)
}
}
/// What the side in `role` signs: the label, its role, the context, then its own HELLO
/// and the other side's.
fn signed(role: Role, context: &str, own: &Hello, other: &Hello) -> Vec<u8> {
let mut bytes = LABEL.to_vec();
bytes.push(role.byte());
bytes.extend_from_slice(&(context.len() as u32).to_be_bytes());
bytes.extend_from_slice(context.as_bytes());
for hello in [own, other] {
bytes.extend_from_slice(&hello.key);
bytes.extend_from_slice(&hello.nonce);
}
bytes
}
/// Our PROOF, as the side in `role`.
pub(crate) fn prove(
key: &NodeKey,
role: Role,
context: &str,
own: &Hello,
other: &Hello,
) -> [u8; PROOF_BYTES] {
key.sign(&signed(role, context, own, other))
}
/// Whether `proof` is the other side's (the one not in `role`) for this handshake.
pub(crate) fn verify(
role: Role,
context: &str,
own: &Hello,
other: &Hello,
proof: &[u8; PROOF_BYTES],
) -> bool {
identity::verify(
&other.key,
&signed(role.other(), context, other, own),
proof,
)
}
/// Runs the handshake on `stream` as the side in `role`, within `timeout`: returns the
/// public key at the other end. `context`: the listener's address, as dialed (module
/// docs).
pub(crate) async fn run<S>(
stream: &mut S,
key: &NodeKey,
role: Role,
context: &str,
timeout: Duration,
) -> Result<[u8; 32]>
where
S: AsyncRead + AsyncWrite + Unpin,
{
let exchange = async {
let own = Hello::new(key)?;
stream.write_all(&own.to_bytes()).await?;
stream.flush().await?;
let mut hello = [0u8; HELLO_BYTES];
stream.read_exact(&mut hello).await?;
let other = Hello::from_bytes(&hello)?;
if other.key == own.key {
bail!("the other end has our own key");
}
stream
.write_all(&prove(key, role, context, &own, &other))
.await?;
stream.flush().await?;
let mut proof = [0u8; PROOF_BYTES];
stream.read_exact(&mut proof).await?;
if !verify(role, context, &own, &other, &proof) {
bail!("the other end's proof doesn't hold");
}
Ok(other.key)
};
tokio::time::timeout(timeout, exchange)
.await
.map_err(|_| anyhow!("no handshake within {timeout:?}"))?
}
#[cfg(test)]
mod tests {
use super::*;
fn key(seed: u8) -> NodeKey {
NodeKey::from_seed([seed; 32])
}
#[test]
fn a_proof_holds_for_its_handshake_only() {
let (a, b) = (key(1), key(2));
let (ha, hb) = (Hello::new(&a).unwrap(), Hello::new(&b).unwrap());
let context = "example.onion:9663";
let proof = prove(&a, Role::Dialer, context, &ha, &hb);
// b, the listener, checks a's proof.
assert!(verify(Role::Listener, context, &hb, &ha, &proof));
// Another context (passed on to another listener).
assert!(!verify(
Role::Listener,
"other.onion:9663",
&hb,
&ha,
&proof
));
// Another connection: b's nonce is a new one.
let hb2 = Hello::new(&b).unwrap();
assert!(!verify(Role::Listener, context, &hb2, &ha, &proof));
// Reflected: a can't be fooled by its own proof as if b had sent it.
assert!(!verify(Role::Dialer, context, &ha, &hb, &proof));
// Someone else's key in the HELLO.
let hc = Hello {
key: key(3).public(),
nonce: ha.nonce,
};
assert!(!verify(Role::Listener, context, &hb, &hc, &proof));
}
#[test]
fn hellos_round_trip_and_versions_are_checked() {
let hello = Hello::new(&key(1)).unwrap();
assert_eq!(Hello::from_bytes(&hello.to_bytes()).unwrap(), hello);
let mut bytes = hello.to_bytes();
bytes[0] = VERSION + 1;
assert!(Hello::from_bytes(&bytes).is_err());
}
#[tokio::test]
async fn both_ends_learn_each_others_key() {
let (mut dialer, mut listener) = tokio::io::duplex(1024);
let (a, b) = (key(1), key(2));
let context = "example.onion:9663";
let (at_a, at_b) = tokio::join!(
run(
&mut dialer,
&a,
Role::Dialer,
context,
Duration::from_secs(5)
),
run(
&mut listener,
&b,
Role::Listener,
context,
Duration::from_secs(5)
),
);
assert_eq!(at_a.unwrap(), b.public());
assert_eq!(at_b.unwrap(), a.public());
}
#[tokio::test]
async fn a_different_context_fails_on_both_ends() {
let (mut dialer, mut listener) = tokio::io::duplex(1024);
let (a, b) = (key(1), key(2));
let (at_a, at_b) = tokio::join!(
run(
&mut dialer,
&a,
Role::Dialer,
"meant.onion:1",
Duration::from_secs(5)
),
run(
&mut listener,
&b,
Role::Listener,
"actual.onion:1",
Duration::from_secs(5)
),
);
assert!(at_a.is_err() && at_b.is_err());
}
}
|