Skip to main content
  1. Posts/

libp2p Handshake - TCP & Noise

·5 mins
Libp2p Rust Networking Tcp Noise Learning
Table of Contents

The guide is written in Rust, using TCP and Noise to complete the handshake process as a aialer with IPFS node. Full example can be found at ahshum/libp2p-handshake-demo

Fundamentals
#

Multiaddr
#

It serves as informative string that stores various address of protocols and details for connecting a node. Examples are

  • /dns4/example.com/tcp/443
  • /ip4/1.2.3.4/tcp/443
  • /dns4/example.com/ip4/1.2.3.4/tcp/443

Doc: multiformats/multiaddr

Protocol Buffers (Protobuf)
#

It is a lightweight, efficient, and platform-neutral mechanism for serializing structured data, developed by Google. As it is highly efficient in both size and speed, the messages in Noise handshake stage are serialized in Protobuf.

Doc: protobuf

Wire Format
#

Wire format refers to the low-level binary encoding used by a protocol to transmit data over a network or store it efficiently. Protobuf is also one of the wire format which ensures that serialized data is compact and optimized for speed, using techniques like varint encoding and tag-length-value (TLV) structures to minimize size and parsing complexity.

More
#

This section may only covered those fundamentals related to the handshake using TCP and Noise protocols, so regards to other protocols I would highly recommend to read through the libp2p doc and have a general idea about connectivity

Handshake Flow
#

Node Identity Setup
#

A key pair should be generated or provided on the first run and save to disk for reusing on resuming the node. It will be used to derive the peer id as the node identity. There are four supported key types.

Doc: Peer Ids and Keys

Initial Messages
#

In this guide, instead of peer discovery, we assume getting one address for connection so will be using that for openning a connection. The target address is /dns4/ipfs/tcp/4001 or /ip4/127.0.0.1/tcp/4001 depends on the mode run the handshake

It is the first stage of protocol negotiation to agree using multistream-select. The wire format of multistream-select messages is maximum at 255 bytes, the message is always followed by a \n (newline character), with the 1st byte for the total message length (include the newline). Meaning that the actual message is sitting between the 2nd to 256th bytes.

Once the connection is opened, both side can send the initial protocol id (i.e. /multistream/1.0.0) (code at net/multistream.rs).

sequenceDiagram participant Me participant Node Me-->>Node: open connection Node->>Me: /multistream/1.0.0 Me->>Node: /multistream/1.0.0

Or

sequenceDiagram participant Me participant Node Me-->>Node: open connection Me->>Node: /multistream/1.0.0 Node->>Me: /multistream/1.0.0

If the received message is not a recognized protocol id, abort the connection. (as of now /multistream/1.0.0 is the only option, so abort the connection if not receiving the protocol id)

Doc: multiformats/multistream-select libp2p/specs/connections

Negotiate Protocols
#

The next step is to negotiate the security protocols. As we initiated the connection, we are responsible to send the protocol id of the security protocol, and in this guide we will be focusing on Noise so the protocol id is /noise. Remember to pack the message in multistream-select format. (code at net/multistream.rs)

sequenceDiagram participant Me participant Node Me<<-->>Node: multistream-select Me->>Node: /noise Node->>Me: /noise

If responder doesn’t support the protocol, na will be receiving as a response.

sequenceDiagram participant Me participant Node Me<<-->>Node: multistream-select Me->>Node: /noise Node->>Me: na Me<<-->>Node: Noise does not support

Noise Handshake
#

The wire format of messages in Noise is different from multistream-select. The first 2 bytes stores the message length, encoded as a 16-bit big-endian unsigned integer. However, the maximum message length as defined in the Noise spec is 65535 bytes.

The handshake message content is in protobuf using

syntax = "proto2";

message NoiseExtensions {
    repeated bytes webtransport_certhashes = 1;
    repeated string stream_muxers = 2;
}

message NoiseHandshakePayload {
  optional bytes identity_key = 1;
  optional bytes identity_sig = 2;
  optional NoiseExtensions extensions = 4;
}

where the identity_key is using

syntax = "proto2";

enum KeyType {
	RSA = 0;
	Ed25519 = 1;
	Secp256k1 = 2;
	ECDSA = 3;
}

message PublicKey {
	required KeyType Type = 1;
	required bytes Data = 2;
}

message PrivateKey {
	required KeyType Type = 1;
	required bytes Data = 2;
}

Additionally, a static key is required for authentication. It should be valid cryptographic key pairs that generated on runtime and should not store to disk.

In libp2p, only XX handshake pattern is supported

XX:
  -> e
  <- e, ee, s, es
  -> s, se

There are more detail explaination of the messages in the docs, so I wouldn’t go deep into it but instead, I would be more focusing on how the messages look like at the implementation level.

e should be your static key and since snow crate (Noise implementation in Rust) takes the static key in the client constructor (and Noise protocol string Noise_XX_25519_ChaChaPoly_SHA256), the 1st stage (code at net/noise.rs) is basically sending an empty payload with

NoiseHandshakePayload {
  identity_key: None,
  identity_sig: None,
  extensions: None,
}

The 2nd stage (code at net/noise.rs#L69-L116) is reading the receiving messages, verify the signature of the remote node and save their public key. The remote-static-key should be retrieved from the Noise client and use it to construct the original message of identity_sig by noise-libp2p-static-key:<remote-static-key>, and verify with the public key from identity_key. Once the message is verified, we can save the remote public key and derive the peer id from there.

The 3rd stage (code at net/noise.rs#L118-L138) is sending our public key to the remote node using the same payload.

let public_key = PublicKey {
  Type: KeyType::Ed25519,
  Data: <public_key>,
}

NoiseHandshakePayload {
  identity_key: public_key.to_protobuf(),
  identity_sig: "noise-libp2p-static-key:<static_key_in_x25519>",
  extensions: None,
}

Remember to serialize/deserialize the NoiseHandshakePayload to protobuf and pack/unpack the message using the wire format.

Doc: Noise Spec Noise Protocol

Noise Transport
#

The handshake should have completed at this stage, but setting up the muxer protocol would secure the channel and be easier to confirm.

All the following messages should happen via the Noise channel using the agreed static keys. Now you will have to multistream-select again and start negotiating the stream multiplexer (code at net/connection.rs).

Summary
#

To get the example code running, follow the instruction of the README. It will guide you to get the environment set up and how to verify the connection.