El protocolo rlpx del análisis del código fuente de Ethereum
Este artículo se refiere principalmente a la documentación oficial de eth: protocolo rlpx
símbolo
X || Y
: Indica la concatenación de X e YX ^ Y
: X e Y bit a bit XORX[:N]
: los primeros N bytes de X[X, Y, Z, ...]
: Codificación recursiva RLP de [X, Y, Z, …]keccak256(MESSAGE)
: El algoritmo hash keccak256 utilizado por Ethereumecies.encrypt(PUBKEY, MESSAGE, AUTHDATA)
: La función de cifrado de autenticación asimétrica AUTHDATA utilizada por RLPx son los datos de autenticación, no forman parte del texto cifrado, pero AUTHDATA se escribirá en la función hash HMAC-256 antes de generar la etiqueta del mensaje.ecdh.agree(PRIVKEY, PUBKEY)
: es la función de negociación Diffie-Hellman de curva elíptica entre PRIVKEY y PUBKEY
Cifrado ECIES
El cifrado asimétrico ECIES (Esquema de cifrado integrado de curva elíptica) se utiliza para el protocolo de enlace RLPx. El sistema de cifrado utilizado por RLPx:
- Curva elíptica punto base secp256k1
G
KDF(k, len)
: Función de derivación de claves NIST SP 800-56 ConcatenaciónMAC(k, m)
: Función HMAC, usando hash SHA-256AES(k, iv, m)
: función de cifrado simétrico AES-128, modo CTR
Supongamos que Alice quiere enviar un mensaje cifrado a Bob y espera que Bob pueda descifrarlo con su clave privada estática kB
. Alice conoce la clave pública estática de Bob KB
.
Alice para m
cifrar el mensaje:
- Genere un número aleatorio
r
y genere la clave pública de curva elíptica correspondienteR = r * G
- Calcule el secreto compartido
S = Px
, donde(Px, Py) = r * KB
kE || kM = KDF(S, 32)
Derivación de claves y vectores aleatorios necesarios para el cifrado y la autenticacióniv
- Cifrar con AES
c = AES(kE, iv, m)
- Calcular suma de comprobación MAC
d = MAC(keccak256(kM), iv || c)
- Enviar el texto cifrado completo
R || iv || c || d
a Bob
R || iv || c || d
Bob descifra el texto cifrado :
- derivar el secreto compartido
S = Px
, donde(Px, Py) = r * KB = kB * R
- Derivación de la clave para el cifrado y la autenticación
kE || kM = KDF(S, 32)
- Verificar MAC
d = MAC(keccak256(kM), iv || c)
- obtener texto sin formato
m = AES(kE, iv || c)
identidad del nodo
Todas las operaciones criptográficas se basan en la curva elíptica secp256k1 . Cada nodo mantiene una clave privada estática secp256k1 . Se recomienda que la clave privada solo se restablezca manualmente (por ejemplo, eliminando un archivo o una entrada de la base de datos).
proceso de apretón de manos
La conexión RLPx se basa en la comunicación TCP, y cada comunicación generará una clave efímera aleatoria para el cifrado y la autenticación. El proceso de generar una clave temporal se llama "apretón de manos", y el apretón de manos se realiza entre el iniciador (el nodo que inicia la solicitud de conexión TCP) y el destinatario (el nodo que acepta la conexión).
- El iniciador inicia una conexión TCP al receptor y envía
auth
un mensaje - El receptor acepta la conexión, descifra, verifica
auth
el mensaje (verifique la recuperación de la firma ==keccak256(ephemeral-pubk)
) - El receptor genera mensajes a través de y
remote-ephemeral-pubk
nonce
auth-ack
- El extremo receptor deriva la clave y envía el primer marco de datos (marco) que contiene el mensaje de saludo
- El iniciador recibe
auth-ack
el mensaje y deriva la clave. - El iniciador envía el primer marco de datos cifrados, incluido el mensaje de saludo del iniciador.
- El receptor recibe y verifica el primer marco de datos cifrados
- El iniciador recibe y verifica el primer marco de datos cifrados
- Si se verifica el MAC de la primera trama de datos cifrada en ambos lados, se completa el intercambio de cifrado
Si falla la validación del primer marco de datos, cualquiera de las partes puede desconectarse.
mensaje de apretón de manos
remitente:
auth = auth-size || enc-auth-body
auth-size = size of enc-auth-body, encoded as a big-endian 16-bit integer
auth-vsn = 4
auth-body = [sig, initiator-pubk, initiator-nonce, auth-vsn, ...]
enc-auth-body = ecies.encrypt(recipient-pubk, auth-body || auth-padding, auth-size)
auth-padding = arbitrary data
Extremo de recepción:
ack = ack-size || enc-ack-body
ack-size = size of enc-ack-body, encoded as a big-endian 16-bit integer
ack-vsn = 4
ack-body = [recipient-ephemeral-pubk, recipient-nonce, ack-vsn, ...]
enc-ack-body = ecies.encrypt(initiator-pubk, ack-body || ack-padding, ack-size)
ack-padding = arbitrary data
Las implementaciones deben ignorar todas las discrepancias enauth-vsn
y .ack-vsn
Las implementaciones deben ignorar todos los elementos de lista adicionales enauth-body
y .ack-body
Después de que se intercambian los mensajes de protocolo de enlace, se genera la clave:
static-shared-secret = ecdh.agree(privkey, remote-pubk)
ephemeral-key = ecdh.agree(ephemeral-privkey, remote-ephemeral-pubk)
shared-secret = keccak256(ephemeral-key || keccak256(nonce || initiator-nonce))
aes-secret = keccak256(ephemeral-key || shared-secret)
mac-secret = keccak256(ephemeral-key || aes-secret)
estructura del marco
Después del apretón de manos, todos los mensajes se transmiten en tramas. Un marco de datos lleva un mensaje cifrado que pertenece a una determinada función.
El objetivo principal de la trama es admitir de manera confiable los protocolos de multiplexación en una sola conexión. En segundo lugar, los flujos de cifrado se simplifican debido a la trama de paquetes, que crea puntos de demarcación apropiados para los códigos de autenticación de mensajes. El marco de datos se cifra y se autentica con la clave generada por el protocolo de enlace.
El encabezado del marco proporciona información sobre el tamaño del mensaje y las capacidades de la fuente del mensaje. Los bytes de relleno se utilizan para evitar el agotamiento del búfer, de modo que los componentes del marco se alineen con el tamaño de byte de bloque especificado.
frame = header-ciphertext || header-mac || frame-ciphertext || frame-mac
header-ciphertext = aes(aes-secret, header)
header = frame-size || header-data || header-padding
header-data = [capability-id, context-id]
capability-id = integer, always zero
context-id = integer, always zero
header-padding = zero-fill header to 16-byte boundary
frame-ciphertext = aes(aes-secret, frame-data || frame-padding)
frame-padding = zero-fill frame-data to 16-byte boundary
MAC
La autenticación de mensajes en RLPx utiliza dos estados keccak256 para dos direcciones de transmisión respectivamente. egress-mac
y ingress-mac
representan el estado de envío y recepción respectivamente, y el estado se actualizará cada vez que se envíe o reciba el texto cifrado. Después del protocolo de enlace inicial, el estado MAC se inicializa de la siguiente manera:
remitente:
egress-mac = keccak256.init((mac-secret ^ recipient-nonce) || auth)
ingress-mac = keccak256.init((mac-secret ^ initiator-nonce) || ack)
Extremo de recepción:
egress-mac = keccak256.init((mac-secret ^ initiator-nonce) || ack)
ingress-mac = keccak256.init((mac-secret ^ recipient-nonce) || auth)
Al enviar una trama de datos, el estado se actualiza con los datos que se enviarán egress-mac
y luego se calcula el valor MAC correspondiente. Las actualizaciones se realizan mediante XORing en la salida cifrada del encabezado de la trama con su valor MAC correspondiente. Esto se hace para garantizar operaciones uniformes en MAC de texto sin formato y textos cifrados. Todos los valores MAC se envían en texto claro.
header-mac-seed = aes(mac-secret, keccak256.digest(egress-mac)[:16]) ^ header-ciphertext
egress-mac = keccak256.update(egress-mac, header-mac-seed)
header-mac = keccak256.digest(egress-mac)[:16]
calcularframe-mac
egress-mac = keccak256.update(egress-mac, frame-ciphertext)
frame-mac-seed = aes(mac-secret, keccak256.digest(egress-mac)[:16]) ^ keccak256.digest(egress-mac)[:16]
egress-mac = keccak256.update(egress-mac, frame-mac-seed)
frame-mac = keccak256.digest(egress-mac)[:16]
El valor MAC en la trama de entrada se puede verificar siempre que el remitente y el receptor actualicen la suma de la misma manera egress-mac
y comparen los valores de y en la trama de entrada. Este paso debe realizarse antes de descifrar y .ingress-mac
header-mac
frame-mac
header-ciphertext
frame-ciphertext
mensaje de función
Todos los mensajes después del apretón de manos inicial están relacionados con "capacidades". Cualquier número de funciones se puede utilizar simultáneamente en una sola conexión RLPx.
Las funciones se identifican mediante un nombre ASCII corto y un número de versión. Las capacidades admitidas por ambos extremos de la conexión se intercambian en mensajes de saludo que pertenecen a la capacidad "p2p" que debe estar disponible en todas las conexiones.
codificación de mensajes
El mensaje de saludo inicial se codifica de la siguiente manera:
frame-data = msg-id || msg-data
frame-size = length of frame-data, encoded as a 24bit big-endian integer
donde msg-id
es el número entero codificado en RLP que identifica el mensaje y msg-data
es la lista RLP que contiene los datos del mensaje.
Todos los mensajes después de Hello se comprimen utilizando el algoritmo Snappy. Tenga en cuenta que el tamaño de un mensaje comprimido frame-size
se refiere al msg-data
tamaño antes de la compresión. La codificación comprimida del mensaje es:
frame-data = msg-id || snappyCompress(msg-data)
frame-size = length of (msg-id || msg-data) encoded as a 24bit big-endian integer
Basado msg-id
en multiplexación
Aunque se admite en el marco capability-id
, este campo no se usa para multiplexar entre diferentes funciones en esta versión de RLPx (la versión actual solo usa msg-id para lograr la multiplexación).
Cada función asigna tanto espacio de msg-id como sea necesario. El espacio msg-id requerido por todas estas funciones debe especificarse mediante estática. Al conectarse y recibir mensajes de saludo , ambos extremos tienen información de pares sobre las capacidades compartidas (incluidas las versiones) y pueden llegar a un consenso sobre el espacio msg-id.
msg-id debe ser mayor que 0x11 (0x00-0x10 están reservados para la función "p2p").
función p2p
Todas las conexiones son compatibles con "p2p". Después del apretón de manos inicial, ambos extremos de la conexión deben enviar mensajes de saludo o de desconexión . Después de recibir el mensaje de saludo, la sesión se activa y puede comenzar a enviar otros mensajes. Las implementaciones DEBEN ignorar todas las diferencias en las versiones del protocolo debido a la compatibilidad hacia adelante. Al comunicarse con nodos en versiones inferiores, las implementaciones deben intentar mantenerse cerca de esa versión.
Se puede recibir un mensaje de desconexión en cualquier momento .
Hola (0x00)
[protocolVersion: P, clientId: B, capabilities, listenPort: P, nodeKey: B_64, ...]
Una vez que se completa el apretón de manos, el primer paquete de datos enviado por ambas partes. No se pueden enviar otros mensajes hasta que se reciba el mensaje Hello. Las implementaciones DEBEN ignorar todos los demás elementos de la lista en el mensaje de saludo, ya que pueden usarse en una versión futura.
protocolVersion
La versión actual de la función p2p es la versión 5clientId
Representa la identidad del software del cliente, una cadena legible por humanos, como "Ethereum(++)/1.0.0"capabilities
Lista de subprotocolos soportados, sus nombres y sus versiones:[[cap1, capVersion1], [cap2, capVersion2], ...]
listenPort
El puerto de escucha del nodo (la interfaz en la ruta de conexión actual), 0 significa que no escuchanodeId
La clave pública de secp256k1 corresponde a la clave privada del nodo
Desconectar (0x01)
[reason: P]
Notifique al nodo que se desconecte. Después de recibir este mensaje, el nodo debería desconectarse inmediatamente. Si está enviando, el host normal le dará al nodo 2 segundos para leer, de modo que se desconecte activamente.
reason
Un entero opcional que indica el motivo de la desconexión:
Razón | Significado |
---|---|
0x00 |
Desconexión solicitada |
0x01 |
Error del subsistema TCP |
0x02 |
Incumplimiento de protocolo, por ejemplo, un mensaje mal formado, mal RLP, … |
0x03 |
Compañero inútil |
0x04 |
demasiados compañeros |
0x05 |
Ya conectado |
0x06 |
Versión de protocolo P2P incompatible |
0x07 |
Identidad de nodo nula recibida: esto no es válido automáticamente |
0x08 |
Renuncia del cliente |
0x09 |
Identidad inesperada en apretón de manos |
0x0a |
La identidad es la misma que este nodo (es decir, está conectado a sí mismo) |
0x0b |
Tiempo de espera de ping |
0x10 |
Alguna otra razón específica de un subprotocolo |
Hacer ping (0x02)
[]
Solicite al nodo una respuesta Pong inmediata .
Pong (0x03)
[]
Responda al paquete Ping del nodo .
Análisis de código fuente
la función principal
devolver objeto de transferencia
Devuelve un objeto de transporte, la conexión dura 5 segundos.
// handshakeTimeout 5
func newRLPX(fd net.Conn) transport {
....
}
leer el mensaje
Devuelve el objeto Msg, llama a ReadMsg del lector y la conexión dura 30 segundos
func (t *rlpx) ReadMsg() (Msg, error) {
..
t.fd.SetReadDeadline(time.Now().Add(frameReadTimeout))
}
escribe un mensaje
Llame a WriteMsg del lector para escribir información, y la conexión dura 20 segundos
func (t *rlpx) WriteMsg(msg Msg) error {
...
t.fd.SetWriteDeadline(time.Now().Add(frameWriteTimeout))
}
Apretón de manos de la versión del protocolo
Apretón de manos del protocolo, la entrada y la salida son objetos protoHandshake, incluido el número de versión, el nombre, la capacidad, el número de puerto, la identificación y un atributo extendido, que se verificará durante el apretón de manos.
apretón de manos encriptado
El iniciador del apretón de manos se llama iniciador.
El receptor se llama receptor .
Correspondiente a dos métodos de procesamiento iniciadorEncHandshake y receptorEncHandshake respectivamente
Después de que los dos métodos de procesamiento sean exitosos, se obtendrá un objeto de secretos , que guarda la información de la clave compartida, y generará un procesador de cuadros junto con el objeto net.Conn original: rlpxFrameRW
La información utilizada por ambas partes en el protocolo de enlace incluye: sus respectivos pares de direcciones de claves públicas y privadas** (iPrv, iPub, rPrv, rPub) , sus respectivos pares de claves públicas y privadas aleatorias generadas (iRandPrv, iRandPub, rRandPrv, rRandPub) , y sus respectivos números aleatorios temporales generados (initNonce, respNonce).**
donde i comienza con la información del iniciador** (iniciador) , r comienza con la información del receptor (receptor)**.
func (t *rlpx) doEncHandshake(prv *ecdsa.PrivateKey, dial *ecdsa.PublicKey) (*ecdsa.PublicKey, error) {
var (
sec secrets
err error
)
if dial == nil {
sec, err = receiverEncHandshake(t.fd, prv) // 接收者
} else {
sec, err = initiatorEncHandshake(t.fd, prv, dial) //主动发起者
}
...
t.rw = newRLPXFrameRW(t.fd, sec)
t.wmu.Unlock()
return sec.Remote.ExportECDSA(), nil
}
Aquí explicamos el código fuente de la parte activa del protocolo de enlace initiatorEncHandshake
:
①: Inicializar el objeto de apretón de manos
h := &encHandshake{initiator: true, remote: ecies.ImportECDSAPublic(remote)}
②: Generar información de verificación
authMsg, err := h.makeAuthMsg(prv)
func (h *encHandshake) makeAuthMsg(prv *ecdsa.PrivateKey) (*authMsgV4, error) {
// 生成己方随机数initNonce
h.initNonce = make([]byte, shaLen)
_, err := rand.Read(h.initNonce)
...
}
// 生成随机的一组公私钥对
h.randomPrivKey, err = ecies.GenerateKey(rand.Reader, crypto.S256(), nil)
...
}
// 生成静态共享秘密token(用己方私钥和对方公钥进行有限域乘法)
token, err := h.staticSharedSecret(prv)
...
}
// 和己方随机数异或后用随机生成的私钥签名
signed := xor(token, h.initNonce)
signature, err := crypto.Sign(signed, h.randomPrivKey.ExportECDSA())
...
}
...
return msg, nil
}
③: Encapsulación, codificación rlp de la información de verificación y apretón de manos e información de prefijo de empalme
authPacket, err := sealEIP8(authMsg, h)
④: Enviar un mensaje a través de conexión
conn.Write(authPacket)
⑤: Procese la información recibida y obtenga el paquete de respuesta
readHandshakeMsg
más fácil. Intente decodificar primero con un formato. Si no, prueba con otro. Debería ser una configuración de compatibilidad. Básicamente, use su propia clave privada para decodificar y luego llame a rlp para decodificar en una estructura.La descripción de la estructura es la siguiente authRespV4, la más importante de las cuales es la clave pública aleatoria del par. Ambas partes pueden obtener el mismo secreto compartido a través de su propia clave privada y la clave pública aleatoria del extremo opuesto. Y este secreto compartido no está disponible para terceros.
authRespMsg := new(authRespV4)
authRespPacket, err := readHandshakeMsg(authRespMsg, encAuthRespLen, prv, conn)
⑥: Complete el respNonce de la respuesta (el número aleatorio de la otra parte, utilizado para generar la clave privada compartida) y remoteRandomPub (la clave pública aleatoria de la otra parte)
h.handleAuthResp(authRespMsg)
⑦: encapsule el paquete de solicitud y el paquete de respuesta en secretos compartidos (secretos)
h.secrets(authPacket, authRespPacket)
En este punto, el contenido más importante relacionado con RLPX está casi explicado.
referencia
https://github.com/blockchainGuide/blockchainguide ☆ ☆ ☆ ☆ ☆
https://mindcarver.cn/ ☆ ☆ ☆ ☆ ☆
https://github.com/ethereum/devp2p/blob/master/rlpx.md