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
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).
Or
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)
If responder doesn’t support the protocol, na
will be receiving as a response.
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.