Skip to main content

netget/server/devp2p/
rlpx.rs

1//! RLPx: the encrypted, authenticated transport every devp2p connection runs on.
2//!
3//! **Handshake** (EIP-8 form, the only one go-ethereum still accepts). The dialler knows the
4//! listener's node id. It sends `auth` = ECIES-to-the-listener of
5//! `rlp([sig, initiator-id, initiator-nonce, 4])` plus 100–199 bytes of padding, where `sig` is a
6//! recoverable signature by a fresh **ephemeral** key over `static-shared-secret ⊕ nonce`. The
7//! listener recovers the dialler's ephemeral public key from that signature and answers `ack` =
8//! ECIES-to-the-dialler of `rlp([listener-ephemeral-id, listener-nonce, 4])` plus padding. Each
9//! packet is prefixed with its big-endian `u16` length, which is also ECIES's authenticated
10//! shared data. Both packets are bounded at [`MAX_HANDSHAKE_BYTES`] before anything is read for
11//! them.
12//!
13//! **Secrets**. With `ecdhe` the x-coordinate of the two ephemeral keys' ECDH:
14//! `shared = keccak(ecdhe ‖ keccak(resp-nonce ‖ init-nonce))`, `aes = keccak(ecdhe ‖ shared)`,
15//! `mac = keccak(ecdhe ‖ aes)`. The dialler's egress MAC is a running keccak seeded with
16//! `(mac ⊕ resp-nonce) ‖ auth-packet`, its ingress MAC with `(mac ⊕ init-nonce) ‖ ack-packet`;
17//! the listener's are the same two, swapped.
18//!
19//! **Frames**. `header (16, AES-256-CTR) ‖ header-mac (16) ‖ frame (padded to 16, same CTR
20//! stream) ‖ frame-mac (16)`. The header is a 24-bit frame size and `rlp([0, 0])`. Both MACs are
21//! go-ethereum's legacy construction: AES-256-ECB (key `mac`) of the running keccak's digest,
22//! XORed with a seed, fed back into the keccak, digest truncated to 16 bytes. Every MAC is
23//! checked, in constant time, **before** the bytes it covers are decrypted, and a frame's size
24//! is checked against the reader's bound before its body is read.
25//!
26//! **Compression**. Once both Hellos are exchanged and the peer's base-protocol version is 5 or
27//! more, every message payload is a Snappy block. The declared decompressed length is checked
28//! against the bound before a byte is decompressed.
29use super::crypto::{self, NodeKey, ECIES_OVERHEAD};
30use super::rlp::{self, Rlp};
31use aes::cipher::{BlockEncrypt, KeyInit, KeyIvInit, StreamCipher};
32use anyhow::{ensure, Context, Result};
33use rand::Rng;
34use secp256k1::PublicKey;
35use sha3::{Digest, Keccak256};
36use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
37
38/// The largest handshake packet accepted (go-ethereum's own bound).
39pub const MAX_HANDSHAKE_BYTES: usize = 2048;
40/// RLPx frame sizes are 24-bit.
41pub const MAX_FRAME_SIZE: usize = 0xff_ffff;
42/// The frame header's protocol data: `rlp([0, 0])` (capability id and context id, both unused).
43const HEADER_DATA: [u8; 3] = [0xc2, 0x80, 0x80];
44
45type Aes256Ctr = ctr::Ctr128BE<aes::Aes256>;
46
47/// What the handshake produced.
48pub struct Secrets {
49    pub aes: [u8; 32],
50    pub mac: [u8; 32],
51    pub egress_mac: Keccak256,
52    pub ingress_mac: Keccak256,
53    /// The peer's static public key: the one we dialled, or the one the dialler proved.
54    pub remote: PublicKey,
55}
56
57/// A decoded `auth` body.
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct AuthMsg {
60    pub signature: [u8; 65],
61    pub initiator_id: [u8; 64],
62    pub nonce: [u8; 32],
63    pub version: u64,
64}
65
66/// A decoded `ack` body.
67#[derive(Debug, Clone, PartialEq, Eq)]
68pub struct AckMsg {
69    pub ephemeral_id: [u8; 64],
70    pub nonce: [u8; 32],
71    pub version: u64,
72}
73
74fn xor32(a: &[u8; 32], b: &[u8; 32]) -> [u8; 32] {
75    let mut out = [0u8; 32];
76    for i in 0..32 {
77        out[i] = a[i] ^ b[i];
78    }
79    out
80}
81
82/// Decrypt a complete handshake packet (`prefix ‖ ciphertext`) addressed to `key`.
83pub fn open_packet(key: &NodeKey, packet: &[u8]) -> Result<Vec<u8>> {
84    ensure!(packet.len() > 2, "handshake packet is empty");
85    crypto::ecies_decrypt(key.secret(), &packet[2..], &packet[..2])
86}
87
88/// Decode an `auth` body. Extra list elements and trailing padding are ignored, as EIP-8
89/// requires for forward compatibility.
90pub fn parse_auth(plain: &[u8]) -> Result<AuthMsg> {
91    let (item, _) = rlp::decode_prefix(plain).context("auth body is not RLP")?;
92    let l = item.as_list().context("auth body")?;
93    ensure!(l.len() >= 4, "auth body has {} fields, not 4", l.len());
94    Ok(AuthMsg {
95        signature: l[0].as_fixed().context("auth signature")?,
96        initiator_id: l[1].as_fixed().context("auth initiator id")?,
97        nonce: l[2].as_fixed().context("auth nonce")?,
98        version: l[3].as_u64().context("auth version")?,
99    })
100}
101
102/// Decode an `ack` body.
103pub fn parse_ack(plain: &[u8]) -> Result<AckMsg> {
104    let (item, _) = rlp::decode_prefix(plain).context("ack body is not RLP")?;
105    let l = item.as_list().context("ack body")?;
106    ensure!(l.len() >= 3, "ack body has {} fields, not 3", l.len());
107    Ok(AckMsg {
108        ephemeral_id: l[0].as_fixed().context("ack ephemeral key")?,
109        nonce: l[1].as_fixed().context("ack nonce")?,
110        version: l[2].as_u64().context("ack version")?,
111    })
112}
113
114/// Encrypt a handshake body to `remote`, padded and size-prefixed.
115pub fn seal(remote: &PublicKey, body: Rlp) -> Vec<u8> {
116    let mut plain = body.encode();
117    let pad = rand::thread_rng().gen_range(100..200);
118    plain.resize(plain.len() + pad, 0);
119    let size = (plain.len() + ECIES_OVERHEAD) as u16;
120    let prefix = size.to_be_bytes();
121    let mut out = prefix.to_vec();
122    out.extend_from_slice(&crypto::ecies_encrypt(remote, &plain, &prefix));
123    out
124}
125
126/// Read one size-prefixed handshake packet, refusing a size above the bound before reading it.
127pub async fn read_packet<S: AsyncRead + Unpin>(io: &mut S) -> Result<Vec<u8>> {
128    let mut prefix = [0u8; 2];
129    io.read_exact(&mut prefix)
130        .await
131        .context("reading the handshake size")?;
132    let size = usize::from(u16::from_be_bytes(prefix));
133    ensure!(
134        size <= MAX_HANDSHAKE_BYTES,
135        "handshake packet of {size} bytes is above the {MAX_HANDSHAKE_BYTES}-byte bound"
136    );
137    ensure!(
138        size > ECIES_OVERHEAD,
139        "handshake packet of {size} bytes cannot hold an ECIES message"
140    );
141    let mut packet = vec![0u8; 2 + size];
142    packet[..2].copy_from_slice(&prefix);
143    io.read_exact(&mut packet[2..])
144        .await
145        .context("reading the handshake packet")?;
146    Ok(packet)
147}
148
149/// Derive the session secrets once both packets and both nonces are known.
150#[allow(clippy::too_many_arguments)]
151pub fn derive_secrets(
152    initiator: bool,
153    ephemeral: &NodeKey,
154    remote_ephemeral: &PublicKey,
155    init_nonce: &[u8; 32],
156    resp_nonce: &[u8; 32],
157    auth_packet: &[u8],
158    ack_packet: &[u8],
159    remote: PublicKey,
160) -> Secrets {
161    let ecdhe = crypto::ecdh_x(ephemeral.secret(), remote_ephemeral);
162    let shared = crypto::keccak256(&[&ecdhe, &crypto::keccak256(&[resp_nonce, init_nonce])]);
163    let aes = crypto::keccak256(&[&ecdhe, &shared]);
164    let mac = crypto::keccak256(&[&ecdhe, &aes]);
165    let mut mac1 = Keccak256::new();
166    mac1.update(xor32(&mac, resp_nonce));
167    mac1.update(auth_packet);
168    let mut mac2 = Keccak256::new();
169    mac2.update(xor32(&mac, init_nonce));
170    mac2.update(ack_packet);
171    let (egress_mac, ingress_mac) = if initiator {
172        (mac1, mac2)
173    } else {
174        (mac2, mac1)
175    };
176    Secrets {
177        aes,
178        mac,
179        egress_mac,
180        ingress_mac,
181        remote,
182    }
183}
184
185/// Dial side: send `auth`, read `ack`.
186pub async fn initiate<R: AsyncRead + Unpin, W: AsyncWrite + Unpin>(
187    r: &mut R,
188    w: &mut W,
189    key: &NodeKey,
190    remote: &PublicKey,
191) -> Result<Secrets> {
192    let ephemeral = NodeKey::random();
193    let mut init_nonce = [0u8; 32];
194    rand::thread_rng().fill(&mut init_nonce);
195    let token = crypto::ecdh_x(key.secret(), remote);
196    let signature = crypto::sign_recoverable(&xor32(&token, &init_nonce), ephemeral.secret());
197    let auth = seal(
198        remote,
199        Rlp::List(vec![
200            Rlp::bytes(&signature),
201            Rlp::bytes(&key.id()),
202            Rlp::bytes(&init_nonce),
203            Rlp::uint(4),
204        ]),
205    );
206    w.write_all(&auth).await.context("writing auth")?;
207    w.flush().await?;
208    let ack_packet = read_packet(r).await?;
209    let ack = parse_ack(&open_packet(key, &ack_packet).context("decrypting ack")?)?;
210    let remote_ephemeral = crypto::public_from_id(&ack.ephemeral_id).context("ack ephemeral")?;
211    Ok(derive_secrets(
212        true,
213        &ephemeral,
214        &remote_ephemeral,
215        &init_nonce,
216        &ack.nonce,
217        &auth,
218        &ack_packet,
219        *remote,
220    ))
221}
222
223/// The listener's view of a verified `auth`: who dialled, and their ephemeral key.
224pub struct AcceptedAuth {
225    pub auth: AuthMsg,
226    pub initiator: PublicKey,
227    pub remote_ephemeral: PublicKey,
228}
229
230/// Verify an `auth` body: the initiator id is a point, and the signature recovers to a key.
231pub fn accept_auth(key: &NodeKey, auth: AuthMsg) -> Result<AcceptedAuth> {
232    let initiator = crypto::public_from_id(&auth.initiator_id).context("auth initiator id")?;
233    let token = crypto::ecdh_x(key.secret(), &initiator);
234    let remote_ephemeral =
235        crypto::recover(&xor32(&token, &auth.nonce), &auth.signature).context("auth signature")?;
236    Ok(AcceptedAuth {
237        auth,
238        initiator,
239        remote_ephemeral,
240    })
241}
242
243/// Listen side: read `auth`, send `ack`.
244pub async fn respond<R: AsyncRead + Unpin, W: AsyncWrite + Unpin>(
245    r: &mut R,
246    w: &mut W,
247    key: &NodeKey,
248) -> Result<Secrets> {
249    let auth_packet = read_packet(r).await?;
250    let auth = parse_auth(&open_packet(key, &auth_packet).context("decrypting auth")?)?;
251    let accepted = accept_auth(key, auth)?;
252    let ephemeral = NodeKey::random();
253    let mut resp_nonce = [0u8; 32];
254    rand::thread_rng().fill(&mut resp_nonce);
255    let ack = seal(
256        &accepted.initiator,
257        Rlp::List(vec![
258            Rlp::bytes(&ephemeral.id()),
259            Rlp::bytes(&resp_nonce),
260            Rlp::uint(4),
261        ]),
262    );
263    w.write_all(&ack).await.context("writing ack")?;
264    w.flush().await?;
265    Ok(derive_secrets(
266        false,
267        &ephemeral,
268        &accepted.remote_ephemeral,
269        &accepted.auth.nonce,
270        &resp_nonce,
271        &auth_packet,
272        &ack,
273        accepted.initiator,
274    ))
275}
276
277/// One direction's MAC state.
278struct MacState {
279    cipher: aes::Aes256,
280    hash: Keccak256,
281}
282
283impl MacState {
284    fn new(mac_secret: &[u8; 32], hash: Keccak256) -> Self {
285        MacState {
286            cipher: aes::Aes256::new(mac_secret.into()),
287            hash,
288        }
289    }
290
291    fn digest(&self) -> [u8; 32] {
292        self.hash.clone().finalize().into()
293    }
294
295    fn compute(&mut self, sum1: &[u8; 32], seed: &[u8]) -> [u8; 16] {
296        let mut first = [0u8; 16];
297        first.copy_from_slice(&sum1[..16]);
298        let mut block = aes::Block::from(first);
299        self.cipher.encrypt_block(&mut block);
300        for (b, s) in block.iter_mut().zip(seed) {
301            *b ^= s;
302        }
303        self.hash.update(block);
304        let sum2 = self.digest();
305        let mut out = [0u8; 16];
306        out.copy_from_slice(&sum2[..16]);
307        out
308    }
309
310    fn header(&mut self, header_ciphertext: &[u8]) -> [u8; 16] {
311        let sum1 = self.digest();
312        self.compute(&sum1, header_ciphertext)
313    }
314
315    fn frame(&mut self, frame_ciphertext: &[u8]) -> [u8; 16] {
316        self.hash.update(frame_ciphertext);
317        let seed = self.digest();
318        self.compute(&seed, &seed[..16])
319    }
320}
321
322fn equal(a: &[u8], b: &[u8]) -> bool {
323    a.len() == b.len() && a.iter().zip(b).fold(0u8, |d, (x, y)| d | (x ^ y)) == 0
324}
325
326/// Why a frame could not be read.
327#[derive(Debug)]
328pub enum FrameError {
329    Io(std::io::Error),
330    /// A size above the reader's bound, refused before the body was read or decompressed.
331    TooLarge {
332        declared: usize,
333        bound: usize,
334    },
335    /// A MAC mismatch, a bad code, bad Snappy: the peer is not speaking RLPx with us.
336    Protocol(String),
337}
338
339impl std::fmt::Display for FrameError {
340    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
341        match self {
342            FrameError::Io(e) => write!(f, "{e}"),
343            FrameError::TooLarge { declared, bound } => {
344                write!(f, "{declared} bytes declared, above the {bound}-byte bound")
345            }
346            FrameError::Protocol(m) => write!(f, "{m}"),
347        }
348    }
349}
350
351impl std::error::Error for FrameError {}
352
353impl From<std::io::Error> for FrameError {
354    fn from(e: std::io::Error) -> Self {
355        FrameError::Io(e)
356    }
357}
358
359/// The write half of a session's frame codec.
360pub struct FrameWriter {
361    enc: Aes256Ctr,
362    mac: MacState,
363    snappy: bool,
364}
365
366/// The read half.
367pub struct FrameReader {
368    dec: Aes256Ctr,
369    mac: MacState,
370    snappy: bool,
371    /// The largest message accepted, after decompression; frames are bounded by the same
372    /// number plus Snappy's worst-case expansion.
373    max_message: usize,
374}
375
376/// Split the secrets into the two halves of the frame codec.
377pub fn codec(secrets: Secrets, max_message: usize) -> (FrameReader, FrameWriter) {
378    let iv = [0u8; 16];
379    let reader = FrameReader {
380        dec: Aes256Ctr::new(&secrets.aes.into(), &iv.into()),
381        mac: MacState::new(&secrets.mac, secrets.ingress_mac),
382        snappy: false,
383        max_message,
384    };
385    let writer = FrameWriter {
386        enc: Aes256Ctr::new(&secrets.aes.into(), &iv.into()),
387        mac: MacState::new(&secrets.mac, secrets.egress_mac),
388        snappy: false,
389    };
390    (reader, writer)
391}
392
393/// The largest frame a reader with this message bound accepts.
394pub fn max_frame_for(max_message: usize) -> usize {
395    (snap::raw::max_compress_len(max_message) + 16).min(MAX_FRAME_SIZE)
396}
397
398impl FrameWriter {
399    pub fn set_snappy(&mut self, on: bool) {
400        self.snappy = on;
401    }
402
403    /// Encode and encrypt one message.
404    pub fn encode(&mut self, code: u64, payload: &[u8]) -> Result<Vec<u8>> {
405        let compressed;
406        let payload = if self.snappy {
407            compressed = snap::raw::Encoder::new()
408                .compress_vec(payload)
409                .context("snappy compression")?;
410            &compressed[..]
411        } else {
412            payload
413        };
414        let mut frame = Rlp::uint(code).encode();
415        frame.extend_from_slice(payload);
416        let fsize = frame.len();
417        ensure!(
418            fsize <= MAX_FRAME_SIZE,
419            "a {fsize}-byte frame does not fit RLPx's 24-bit size"
420        );
421        let mut header = [0u8; 16];
422        header[..3].copy_from_slice(&(fsize as u32).to_be_bytes()[1..]);
423        header[3..6].copy_from_slice(&HEADER_DATA);
424        self.enc.apply_keystream(&mut header);
425        let header_mac = self.mac.header(&header);
426        frame.resize(fsize.div_ceil(16) * 16, 0);
427        self.enc.apply_keystream(&mut frame);
428        let frame_mac = self.mac.frame(&frame);
429        let mut out = Vec::with_capacity(32 + frame.len() + 16);
430        out.extend_from_slice(&header);
431        out.extend_from_slice(&header_mac);
432        out.extend_from_slice(&frame);
433        out.extend_from_slice(&frame_mac);
434        Ok(out)
435    }
436}
437
438impl FrameReader {
439    pub fn set_snappy(&mut self, on: bool) {
440        self.snappy = on;
441    }
442
443    /// Read, verify and decrypt one message: its code and its (decompressed) payload.
444    pub async fn read<R: AsyncRead + Unpin>(
445        &mut self,
446        r: &mut R,
447    ) -> std::result::Result<(u64, Vec<u8>), FrameError> {
448        let mut head = [0u8; 32];
449        r.read_exact(&mut head).await?;
450        let want = self.mac.header(&head[..16]);
451        if !equal(&want, &head[16..]) {
452            return Err(FrameError::Protocol("bad frame header MAC".into()));
453        }
454        self.dec.apply_keystream(&mut head[..16]);
455        let fsize =
456            (usize::from(head[0]) << 16) | (usize::from(head[1]) << 8) | usize::from(head[2]);
457        let bound = max_frame_for(self.max_message);
458        if fsize > bound {
459            return Err(FrameError::TooLarge {
460                declared: fsize,
461                bound,
462            });
463        }
464        let padded = fsize.div_ceil(16) * 16;
465        let mut frame = vec![0u8; padded + 16];
466        r.read_exact(&mut frame).await?;
467        let (body, mac) = frame.split_at_mut(padded);
468        let want = self.mac.frame(body);
469        if !equal(&want, mac) {
470            return Err(FrameError::Protocol("bad frame MAC".into()));
471        }
472        self.dec.apply_keystream(body);
473        let body = &body[..fsize];
474        let (code, payload) =
475            rlp::split_code(body).map_err(|e| FrameError::Protocol(format!("{e:#}")))?;
476        if !self.snappy {
477            if payload.len() > self.max_message {
478                return Err(FrameError::TooLarge {
479                    declared: payload.len(),
480                    bound: self.max_message,
481                });
482            }
483            return Ok((code, payload.to_vec()));
484        }
485        let size = snap::raw::decompress_len(payload)
486            .map_err(|e| FrameError::Protocol(format!("snappy: {e}")))?;
487        if size > self.max_message {
488            return Err(FrameError::TooLarge {
489                declared: size,
490                bound: self.max_message,
491            });
492        }
493        let plain = snap::raw::Decoder::new()
494            .decompress_vec(payload)
495            .map_err(|e| FrameError::Protocol(format!("snappy: {e}")))?;
496        Ok((code, plain))
497    }
498}