El protocolo rlpx del análisis del código fuente de Ethereum

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 Y
  • X ^ Y: X e Y bit a bit XOR
  • X[: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 Ethereum
  • ecies.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 secp256k1G
  • KDF(k, len): Función de derivación de claves NIST SP 800-56 Concatenación
  • MAC(k, m): Función HMAC, usando hash SHA-256
  • AES(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 mcifrar el mensaje:

  1. Genere un número aleatorio ry genere la clave pública de curva elíptica correspondienteR = r * G
  2. Calcule el secreto compartido S = Px, donde(Px, Py) = r * KB
  3. kE || kM = KDF(S, 32)Derivación de claves y vectores aleatorios necesarios para el cifrado y la autenticacióniv
  4. Cifrar con AESc = AES(kE, iv, m)
  5. Calcular suma de comprobación MACd = MAC(keccak256(kM), iv || c)
  6. Enviar el texto cifrado completo R || iv || c || da Bob

R || iv || c || dBob descifra el texto cifrado :

  1. derivar el secreto compartido S = Px, donde(Px, Py) = r * KB = kB * R
  2. Derivación de la clave para el cifrado y la autenticaciónkE || kM = KDF(S, 32)
  3. Verificar MACd = MAC(keccak256(kM), iv || c)
  4. obtener texto sin formatom = 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).

  1. El iniciador inicia una conexión TCP al receptor y envía authun mensaje
  2. El receptor acepta la conexión, descifra, verifica authel mensaje (verifique la recuperación de la firma == keccak256(ephemeral-pubk))
  3. El receptor genera mensajes a través de yremote-ephemeral-pubknonceauth-ack
  4. El extremo receptor deriva la clave y envía el primer marco de datos (marco) que contiene el mensaje de saludo
  5. El iniciador recibe auth-ackel mensaje y deriva la clave.
  6. El iniciador envía el primer marco de datos cifrados, incluido el mensaje de saludo del iniciador.
  7. El receptor recibe y verifica el primer marco de datos cifrados
  8. El iniciador recibe y verifica el primer marco de datos cifrados
  9. 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-macy ingress-macrepresentan 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-macy 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-macy comparen los valores de y en la trama de entrada. Este paso debe realizarse antes de descifrar y .ingress-macheader-macframe-macheader-ciphertextframe-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-ides el número entero codificado en RLP que identifica el mensaje y msg-dataes 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-sizese refiere al msg-datatamañ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-iden 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.

  • protocolVersionLa versión actual de la función p2p es la versión 5
  • clientIdRepresenta la identidad del software del cliente, una cadena legible por humanos, como "Ethereum(++)/1.0.0"
  • capabilitiesLista de subprotocolos soportados, sus nombres y sus versiones:[[cap1, capVersion1], [cap2, capVersion2], ...]
  • listenPortEl puerto de escucha del nodo (la interfaz en la ruta de conexión actual), 0 significa que no escucha
  • nodeIdLa 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.

reasonUn 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

readHandshakeMsgmá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

Supongo que te gusta

Origin blog.csdn.net/pulong0748/article/details/110086818
Recomendado
Clasificación