-
Notifications
You must be signed in to change notification settings - Fork 166
RLPx Encryption
This section describes how a peer session is created.
It is assumed that the underlying transport layer has an already established connection between peers. A session is established in two phases. The first phase is peer authentication with an encryption handshake (or crypto handshake) and the second is the base protocol handshake.
The purpose of peer authentication is to establish a secure communication channel by setting up an encrypted, authenticated message stream. And the purpose of the encryption handshake is to exchange ephemeral keys to set initial values for this secure session.
The second phase is to negotiate supported protocols, checking versions and network Ids and described in the DEVp2p wire protocol specification.
From the point of view of one peer we will distinguish connections in two ways. If the connection was initiated by a peer, we say that they are the initiator, and the other peer is receiver. The word remote is used to describe the 'other' peer in a connection when talking from a point of view of a node.
The other distinction is whether the remote peer is known or new. A known peer is one which has previously been connected and for which a corresponding session token is remembered. Session tokens are assumed to be persisted across sessions.
The protocol is somewhat different depending on whether the peer is initiator or receiver on the one hand and whether the remote peer is known or new on the other.
Creating a secure connection consists of the following steps.
- Initiator sends an authentication message to receiver
- Receiver responds with an authentication response message and sets up a secure session
- Initiator checks receiver's response and establishes a secure session
- Receiver and Initiator then send base protocol handshake on the established secure channel.
Either side may disconnect if authentication fails or if the protocol handshake isn’t appropriate.
If the handshake fails, if and only if initiating a connection TO a known peer, then the nodes information should be removed from the node table and the connection MUST NOT be reattempted. Due to the limited IPv4 space and common ISP practices, this is likely a common and normal occurrence, therefore, no other action should occur. If a handshake fails for a connection which is received, no action pertaining to the node table should occur.
All packets following the encryption handshake, including protocol negotiation handshake in step 4, are framed, encrypted and authenticated using the setup negotiated during the encryption handshake.
If the handshakes succeed, the fixed array of protocols supported by both peers will run on the connection paralelly to send and receive messages. Once established, packets are encapsulated as frames which are encrypted using AES-256 in CTR mode. Initial values for the message authentication and cipher are never the same, key material for the session is derived via a KDF and ECDHE-derived shared-secret. ECC uses secp256k1 curve (ECP). It is the purpose of the encryption handshake to negotiate these key values for a new secure session.
Initiator sends the following handshake.
initiator-handshake := ECIES.Encrypt(remote-pubkey, auth)
The initiator handshake is the authentication message auth
encrypted with the remote peer's known public key using ECIES (Elliptic Curve Integrated Encryption System). Using AES256 in CTR mode with full MAC, the ECIES encrypted payload will have the following structure:
Offset | Name | Description |
---|
0 | `ecies-pubkey` | 65 byte representation of the pubkey needed for symmetric cipher
65 | `AES-initial-vector` | 16 byte initial block for AES
81 | `cipher-text` | ECIES ciphertext size identical to plain text (uses AES-256 CTR, blocksize 16)
275 | `ecies-mac` | ECIES message authentication code (256 bit)
307 | **Total**
This authentication message serves to prove the following:
- the owner of the private key wants a connection with the node controlling a particular public key
- that they want the connection now based on common history
- ...
and has the following structure:
Offset | Name | Description |
---|
0 | `signature` | `Sign(ecdhe-random-key, shared-secret ^ init_nonce)`
65 | `ephemeral-key-mac` | `SHA3(initiator-ecdhe-random-pubkey)` (256 bit)
97 | `permanent-public-key` | NodeId public key (512 bit)
161 | `initiator-nonce` | a random key generated by the initiator for this session
193 | `known-peer` | flag indicating if a token was used as shared secret, 0x00: no, 0x01: yes
194 | **Total
The signature part is there to check if both parties agree on the same shared secret. Initiator signs the shared secret with an ephemeral key using standard ECDSA with P256 Curve. The signature length is therefore exactly 65 bytes.
signature := Sign(ecdhe-random-key, shared-secret ^ init_nonce)
The shared secret is xor-ed with a random nonce to make signatures non-reusable. Since the nonce is in the decrypted auth message and the receiver owns and agrees on a shared secret with initiator, the message text can be reconstructed by the receiver. Therefore the receiver is able to verify the signature and recover initiator's ephemeral public key using EC Recover.
What the shared secret actually is depends on whether the remote peer is a known peer or not. If a remote's public key is associated with a last session token, then that token is used as shared secret. If the peer is new, the initial shared secret is generated by ECDH based on the nodes' public keys.
shared-secret = session_token // for known peer
shared-secret = SSK(initiator-privkey, receiver-pubkey) // for new peers
Where SSK(initiator-privkey, receiver-pubkey)
is a symmetric shared secret key as given by ECDH.
Whether a token was used is indicated in the last byte (known-peer
) of the handshake.
ephemeral-key-mac
is to authenticate the signature and is a Sha3 hash of initiator's ephemeral public key.
permanent-public-key
is used to facilitate lookup of a token, if IP address and port is not reliable.
initiator-nonce
is a 32 byte random nonce, used to derive session key material.
known-peer
is a byte indicating if a previous session token is used (0x00 if not, 0x01 if yes), so basically signal whether the initiator recognises the receiver as a known peer.
Receiver receives the initiator handshake and decrypts it with its own public key. If the initiator indicated the use of a session token (last byte is set to 0x01) and the receiver finds it (under persistent-public-key
or otherwise), then it can recover initiator's ephemeral public key with ECrecover by supplying the plain text which is shared_secret^initiator-nonce
:
initiator-ecdhe-random-pubkey = ECRECOVER(signature, shared_secret^initiator-nonce)
once this is done, the recipient has all information in order to set up the secure session. See below.
The receiver handshake is sent encrypted with the initiator's public key using ECIES:
receiver-handshake = ECIES.Encrypt(initiator-pubkey, receiver-handshake)
Offset | Name | Description |
---|
0 | `ecies-pubkey` | 65 byte representation of the pubkey needed for symmetric cipher
65 | `AES-initial-vector` | 16 byte initial block for AES
81 | `cipher-text` | ECIES ciphertext size identical to plain text (uses AES-256 CTR, blocksize 16)
178 | `ecies-mac` | ECIES message authentication code (256 bit)
210 | **Total**
where receiver-handshake
has the following structure:
Offset | Name | Description |
---|---|---|
0 | receiver-ecdhe-random-pubkey |
newly generated ephemeral key to serve as basis for session-secret |
64 | receiver-nonce |
a randomly generated nonce |
96 | receiver-known-peer |
if receiver has found session token for initiator (0x00) or not (0x01) |
97 | Total |
It is somewhat unclear what the expected behaviour is if initiator submits an auth with session token, but receiver does not remember it and responds with 0x00. Should the connection be terminated or is there a fallback to shared secret?
Originator needs to inspect the receiver handshake response to recover receiver-ecdhe-random-pubkey
and receiver-nonce
. And this completes the key exchange.
Once the encryption handshake's been completed, both parties can calculate the same initial values for encryption and authentication.
First we need the remote ephemeral key to use it with ECDH. From the ecdhe key we derive a shared secret.
ecdhe-shared-secret = ecdh.agree(ecdhe-random-key, remote-ecdhe-random-pubkey)
shared-secret = sha3(ecdhe-shared-secret || sha3(receiver-nonce || initiator-nonce))
The session token is updated to be the sha3 hash of the new shared secret and need to be remembered.
token = sha3(shared-secret)
It is unclear at exactly what point we should update this token, ie., shall we somehow wait for the baseprotocol handshake to complete as well?
The other parameters are only relevant for this session:
aes-secret = sha3(ecdhe-shared-secret || shared-secret)
mac-secret = sha3(ecdhe-shared-secret || aes-secret)
initiator-mac = sha3(mac-secret^receiver-nonce || init-auth)
receiver-mac = sha3(mac-secret^initiator-nonce || init-auth)
Note that if any of these calculations do not match for the peers, frame authentication will fail immediately, so the connection will be terminated.
The connection is secured by encryption and authentication. Encryption and authentication is done frame by frame separately for headers and payload.
Authentication is using Keyed-Hash Message Authentication Code (HMAC FIPS198) using SHA256. An HMAC is set up for incoming (ingress) and outgoing (egress) traffic.
Egress MAC is updated with the plaintext of the outgoing frame payload and the checksum is sent appended to the frame (in cleartext).
Ingress MAC is updated with the plaintext of the decrypted frame payload and the checksum is verified against the MAC received appended to the frame. If they do not match, the datastream has been tampered with, the connection must be terminated. If they match, it proves the authenticity of the frame since an attacker would need the entire history of this session as well as the ephemeral keys exchanged during the handshake even.
For the MACs to match, we need to ensure that initiator's egress MAC is identical to receiver's ingress MAC, and initiator's ingress MAC is identical to receiver's egress MAC. So given the key definitions at the end of the handshake, initiator initialises its egress-mac
with initiator-mac
and its ingress-mac
with receiver-mac
, whereas receiver initialises its egress-mac
with receiver-mac
and its ingress-mac
with initiator-mac
.
Encryption uses the AES-256 block cipher in CTR mode using aes-secret
as key and initial vector ?
❤️ Stay Classy