diff --git a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/connections/netty/AbstractHandshakeHandler.java b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/connections/netty/AbstractHandshakeHandler.java index 1a1ac4e7420..42c2e7cf8e9 100644 --- a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/connections/netty/AbstractHandshakeHandler.java +++ b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/connections/netty/AbstractHandshakeHandler.java @@ -130,7 +130,8 @@ protected final void channelRead0(final ChannelHandlerContext ctx, final ByteBuf metricsSystem, inboundInitiated, peerLookup, - maxMessageSize); + maxMessageSize, + nodeId); ctx.channel() .pipeline() diff --git a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/connections/netty/DeFramer.java b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/connections/netty/DeFramer.java index adec6182e7c..f3f49f606ba 100644 --- a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/connections/netty/DeFramer.java +++ b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/connections/netty/DeFramer.java @@ -45,7 +45,6 @@ import java.io.IOException; import java.net.InetSocketAddress; import java.util.List; -import java.util.Objects; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicBoolean; @@ -74,6 +73,7 @@ final class DeFramer extends ByteToMessageDecoder { private final List subProtocols; private final boolean inboundInitiated; private final PeerLookup peerLookup; + private final Bytes authenticatedNodeId; private boolean hellosExchanged; private final LabelledMetric outboundMessagesCounter; private final int maxMessageSize; @@ -89,7 +89,8 @@ final class DeFramer extends ByteToMessageDecoder { final MetricsSystem metricsSystem, final boolean inboundInitiated, final PeerLookup peerLookup, - final int maxMessageSize) { + final int maxMessageSize, + final Bytes authenticatedNodeId) { this.framer = framer; this.subProtocols = subProtocols; this.localNode = localNode; @@ -99,6 +100,7 @@ final class DeFramer extends ByteToMessageDecoder { this.inboundInitiated = inboundInitiated; this.peerLookup = peerLookup; this.maxMessageSize = maxMessageSize; + this.authenticatedNodeId = authenticatedNodeId; this.outboundMessagesCounter = metricsSystem.createLabelledCounter( BesuMetricCategory.NETWORK, @@ -158,6 +160,17 @@ protected void decode(final ChannelHandlerContext ctx, final ByteBuf in, final L return; } LOG.trace("Received HELLO message: {}", peerInfo); + if (!peerInfo.getNodeId().equals(authenticatedNodeId)) { + LOG.debug( + "Peer Hello nodeId {} does not match authenticated nodeId from handshake {}. Disconnecting.", + peerInfo.getNodeId(), + authenticatedNodeId); + connectFuture.completeExceptionally( + new UnexpectedPeerConnectionException( + "Hello nodeId does not match handshake identity")); + ctx.close(); + return; + } if (peerInfo.getVersion() >= 5) { LOG.trace("Enable compression for p2pVersion: {}", peerInfo.getVersion()); framer.enableCompression(); @@ -200,17 +213,6 @@ protected void decode(final ChannelHandlerContext ctx, final ByteBuf in, final L outboundBytesCounter, inboundInitiated); - // Check peer is who we expected - if (expectedPeer.isPresent() - && !Objects.equals(expectedPeer.get().getId(), peerInfo.getNodeId())) { - final String unexpectedMsg = - String.format( - "Expected id %s, but got %s", expectedPeer.get().getId(), peerInfo.getNodeId()); - connectFuture.completeExceptionally(new UnexpectedPeerConnectionException(unexpectedMsg)); - LOG.debug("{}. Disconnecting.", unexpectedMsg); - connection.disconnect(DisconnectMessage.DisconnectReason.UNEXPECTED_ID); - } - // Check that we have shared caps if (capabilityMultiplexer.getAgreedCapabilities().isEmpty()) { LOG.debug("Disconnecting because no capabilities are shared: {}", peerInfo); diff --git a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/handshake/ecies/ECIESHandshaker.java b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/handshake/ecies/ECIESHandshaker.java index 0538e36aade..e7eff572777 100644 --- a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/handshake/ecies/ECIESHandshaker.java +++ b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/handshake/ecies/ECIESHandshaker.java @@ -54,11 +54,7 @@ public class ECIESHandshaker implements Handshaker { private static final Logger LOG = LoggerFactory.getLogger(ECIESHandshaker.class); private static final SecureRandom RANDOM = SecureRandomProvider.publicSecureRandom(); - static final int SIGNATURE_LENGTH = 65; - static final int HASH_EPH_PUBKEY_LENGTH = 32; static final int PUBKEY_LENGTH = 64; - static final int NONCE_LENGTH = 32; - static final int TOKEN_FLAG_LENGTH = 1; // Keypairs under our control. private NodeKey nodeKey; @@ -86,8 +82,6 @@ public class ECIESHandshaker implements Handshaker { new AtomicReference<>(Handshaker.HandshakeStatus.UNINITIALIZED); private HandshakeSecrets secrets; - private boolean version4 = true; - private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithmFactory.getInstance(); @Override @@ -130,21 +124,11 @@ public ByteBuf firstMessage() throws HandshakeException { "illegal invocation of firstMessage, handshake had already started"); final Bytes32 staticSharedSecret = nodeKey.calculateECDHKeyAgreement(partyPubKey); - if (version4) { - initiatorMsg = - InitiatorHandshakeMessageV4.create( - nodeKey.getPublicKey(), ephKeyPair, staticSharedSecret, initiatorNonce); - } else { - initiatorMsg = - InitiatorHandshakeMessageV1.create( - nodeKey.getPublicKey(), ephKeyPair, staticSharedSecret, initiatorNonce, false); - } + initiatorMsg = + InitiatorHandshakeMessageV4.create( + nodeKey.getPublicKey(), ephKeyPair, staticSharedSecret, initiatorNonce); try { - if (version4) { - initiatorMsgEnc = EncryptedMessage.encryptMsgEip8(initiatorMsg.encode(), partyPubKey); - } else { - initiatorMsgEnc = EncryptedMessage.encryptMsg(initiatorMsg.encode(), partyPubKey); - } + initiatorMsgEnc = EncryptedMessage.encryptMsgEip8(initiatorMsg.encode(), partyPubKey); } catch (final InvalidCipherTextException e) { status.set(Handshaker.HandshakeStatus.FAILED); throw new HandshakeException("Encrypting the first handshake message failed", e); @@ -161,51 +145,26 @@ public Optional handleMessage(final ByteBuf buf) throws HandshakeExcept status.get() == Handshaker.HandshakeStatus.IN_PROGRESS, "illegal invocation of onMessage on handshake that is not in progress"); - // Take as many bytes as expected in the next message. - int expectedLength = ECIESEncryptionEngine.ENCRYPTION_OVERHEAD; - expectedLength += - initiator - ? ResponderHandshakeMessageV1.MESSAGE_LENGTH - : InitiatorHandshakeMessageV1.MESSAGE_LENGTH; - - if (buf.readableBytes() < expectedLength) { - buf.markReaderIndex(); - final int size = buf.readUnsignedShort(); - if (size > buf.readableBytes() + 2) { - buf.resetReaderIndex(); - return Optional.empty(); - } - expectedLength = size; + // Read the EIP-8 size prefix to determine the full message length. + buf.markReaderIndex(); + if (buf.readableBytes() < 2) { + return Optional.empty(); + } + final int size = buf.readUnsignedShort(); + if (size > buf.readableBytes()) { buf.resetReaderIndex(); + return Optional.empty(); } - buf.markReaderIndex(); - final ByteBuf bufferedBytes = buf.readSlice(expectedLength); - final byte[] encryptedBytes = new byte[bufferedBytes.readableBytes()]; - bufferedBytes.getBytes(0, encryptedBytes); - Bytes bytes = Bytes.wrap(encryptedBytes); + // Read the full EIP-8 message (size prefix + payload). + buf.resetReaderIndex(); + final byte[] fullMessage = new byte[size + 2]; + buf.readBytes(fullMessage); + final Bytes encryptedMsg = Bytes.wrap(fullMessage); - Bytes encryptedMsg = bytes; + final Bytes bytes; try { - // Decrypt the message with our private key. - try { - // Assume new format - final int size = bufferedBytes.readUnsignedShort(); - if (buf.writerIndex() >= size) { - bufferedBytes.readerIndex(0); - final byte[] fullMessage = new byte[size + 2]; - bufferedBytes.readBytes(fullMessage, 0, expectedLength); - buf.readBytes(fullMessage, expectedLength, size - expectedLength + 2); - encryptedMsg = Bytes.wrap(fullMessage); - bytes = EncryptedMessage.decryptMsgEIP8(encryptedMsg, nodeKey); - version4 = true; - } else { - throw new HandshakeException("Failed to decrypt handshake message"); - } - } catch (final Exception ex) { - bytes = EncryptedMessage.decryptMsg(bytes, nodeKey); - version4 = false; - } + bytes = EncryptedMessage.decryptMsgEIP8(encryptedMsg, nodeKey); } catch (final InvalidCipherTextException e) { status.set(Handshaker.HandshakeStatus.FAILED); throw new HandshakeException("Decrypting an incoming handshake message failed", e); @@ -226,11 +185,7 @@ public Optional handleMessage(final ByteBuf buf) throws HandshakeExcept // Store the message, as we need it to generating our ingress and egress MACs. responderMsgEnc = encryptedMsg; - if (version4) { - responderMsg = ResponderHandshakeMessageV4.decode(bytes); - } else { - responderMsg = ResponderHandshakeMessageV1.decode(bytes); - } + responderMsg = ResponderHandshakeMessageV4.decode(bytes); // Extract the responder's nonce and ephemeral pubkey, which will be used to generate the // shared secrets. @@ -253,11 +208,7 @@ public Optional handleMessage(final ByteBuf buf) throws HandshakeExcept // Store the message, as we need it to generating our ingress and egress MACs. initiatorMsgEnc = encryptedMsg; try { - if (version4) { - initiatorMsg = InitiatorHandshakeMessageV4.decode(bytes, nodeKey); - } else { - initiatorMsg = InitiatorHandshakeMessageV1.decode(bytes, nodeKey); - } + initiatorMsg = InitiatorHandshakeMessageV4.decode(bytes, nodeKey); } catch (final SecurityModuleException e) { status.set(Handshaker.HandshakeStatus.FAILED); throw new HandshakeException( @@ -279,13 +230,7 @@ public Optional handleMessage(final ByteBuf buf) throws HandshakeExcept "keccak hash of recovered ephemeral pubkey does not match announced hash"); // Build the response message. - if (version4) { - responderMsg = - ResponderHandshakeMessageV4.create(ephKeyPair.getPublicKey(), responderNonce); - } else { - responderMsg = - ResponderHandshakeMessageV1.create(ephKeyPair.getPublicKey(), responderNonce, false); - } + responderMsg = ResponderHandshakeMessageV4.create(ephKeyPair.getPublicKey(), responderNonce); LOG.trace( "Generated responder's ECIES handshake message against peer {}...: {}", @@ -293,11 +238,7 @@ public Optional handleMessage(final ByteBuf buf) throws HandshakeExcept responderMsg); try { - if (version4) { - responderMsgEnc = EncryptedMessage.encryptMsgEip8(responderMsg.encode(), partyPubKey); - } else { - responderMsgEnc = EncryptedMessage.encryptMsg(responderMsg.encode(), partyPubKey); - } + responderMsgEnc = EncryptedMessage.encryptMsgEip8(responderMsg.encode(), partyPubKey); } catch (final InvalidCipherTextException e) { status.set(Handshaker.HandshakeStatus.FAILED); throw new HandshakeException("Encrypting the next handshake message failed", e); @@ -435,14 +376,4 @@ Bytes32 getResponderNonce() { void setResponderNonce(final Bytes32 responderNonce) { this.responderNonce = responderNonce; } - - @VisibleForTesting - void setInitiatorMsgEnc(final Bytes initiatorMsgEnc) { - this.initiatorMsgEnc = initiatorMsgEnc; - } - - @VisibleForTesting - void setResponderMsgEnc(final Bytes responderMsgEnc) { - this.responderMsgEnc = responderMsgEnc; - } } diff --git a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/handshake/ecies/EncryptedMessage.java b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/handshake/ecies/EncryptedMessage.java index c3e84173646..4bb3d5a5b74 100644 --- a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/handshake/ecies/EncryptedMessage.java +++ b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/handshake/ecies/EncryptedMessage.java @@ -40,34 +40,7 @@ private EncryptedMessage() { } /** - * Decrypts the ciphertext using our private key. - * - * @param msgBytes The ciphertext. - * @param nodeKey Abstraction of this nodes private key & associated cryptographic operations - * @return The plaintext. - * @throws InvalidCipherTextException Thrown if decryption failed. - */ - public static Bytes decryptMsg(final Bytes msgBytes, final NodeKey nodeKey) - throws InvalidCipherTextException { - - // Extract the ephemeral public key, stripping off the first byte (0x04), which designates it's - // an uncompressed key. - final SECPPublicKey ephPubKey = SIGNATURE_ALGORITHM.createPublicKey(msgBytes.slice(1, 64)); - - // Strip off the IV to use. - final Bytes iv = msgBytes.slice(65, IV_SIZE); - - // Extract the encrypted payload. - final Bytes encrypted = msgBytes.slice(65 + IV_SIZE); - - // Perform the decryption. - final ECIESEncryptionEngine decryptor = - ECIESEncryptionEngine.forDecryption(nodeKey, ephPubKey, iv); - return decryptor.decrypt(encrypted); - } - - /** - * Decrypts the ciphertext using our private key. + * Decrypts an EIP-8 formatted ciphertext using our private key. * * @param msgBytes The ciphertext. * @param nodeKey Abstraction of this nodes private key & associated cryptographic operations @@ -91,39 +64,7 @@ public static Bytes decryptMsgEIP8(final Bytes msgBytes, final NodeKey nodeKey) } /** - * Encrypts a message for the specified peer using ECIES. - * - * @param bytes The plaintext. - * @param remoteKey The peer's remote key. - * @return The ciphertext. - * @throws InvalidCipherTextException Thrown if encryption failed. - */ - public static Bytes encryptMsg(final Bytes bytes, final SECPPublicKey remoteKey) - throws InvalidCipherTextException { - // TODO: check size. - final ECIESEncryptionEngine engine = ECIESEncryptionEngine.forEncryption(remoteKey); - - // Do the encryption. - final Bytes encrypted = engine.encrypt(bytes); - final Bytes iv = engine.getIv(); - final SECPPublicKey ephPubKey = engine.getEphPubKey(); - - // Create the output message by concatenating the ephemeral public key (prefixed with - // 0x04 to designate uncompressed), IV, and encrypted bytes. - final MutableBytes answer = - MutableBytes.create(1 + ECIESHandshaker.PUBKEY_LENGTH + IV_SIZE + encrypted.size()); - - int offset = 0; - // Set the first byte as 0x04 to specify it's an uncompressed key. - answer.set(offset, (byte) 0x04); - ephPubKey.getEncodedBytes().copyTo(answer, offset += 1); - iv.copyTo(answer, offset += ECIESHandshaker.PUBKEY_LENGTH); - encrypted.copyTo(answer, offset + iv.size()); - return answer; - } - - /** - * Encrypts a message for the specified peer using ECIES. + * Encrypts a message for the specified peer using EIP-8 ECIES. * * @param message The plaintext. * @param remoteKey The peer's remote key. diff --git a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/handshake/ecies/InitiatorHandshakeMessageV1.java b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/handshake/ecies/InitiatorHandshakeMessageV1.java deleted file mode 100644 index 45c9c6d1453..00000000000 --- a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/handshake/ecies/InitiatorHandshakeMessageV1.java +++ /dev/null @@ -1,193 +0,0 @@ -/* - * Copyright ConsenSys AG. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ -package org.hyperledger.besu.ethereum.p2p.rlpx.handshake.ecies; - -import static com.google.common.base.Preconditions.checkState; - -import org.hyperledger.besu.crypto.Hash; -import org.hyperledger.besu.crypto.KeyPair; -import org.hyperledger.besu.crypto.SECPPublicKey; -import org.hyperledger.besu.crypto.SECPSignature; -import org.hyperledger.besu.crypto.SignatureAlgorithm; -import org.hyperledger.besu.crypto.SignatureAlgorithmFactory; -import org.hyperledger.besu.cryptoservices.NodeKey; - -import org.apache.tuweni.bytes.Bytes; -import org.apache.tuweni.bytes.Bytes32; -import org.apache.tuweni.bytes.MutableBytes; - -/** - * The initiator's handshake message. - * - *

This message must be sent by the party that initiates the RLPX connection, as the first - * message in the handshake protocol. - * - *

Message structure

- * - * The following describes the message structure: - * - *
- *   authInitiator -> E(remote-pubk,
- *                      S(ephemeral-privk, static-shared-secret ^ nonce)
- *                       || H(ephemeral-pubk)
- *                      || pubk
- *                      || nonce
- *                      || 0x0)
- * 
- * - * @see Structure of the - * initiator request - */ -public final class InitiatorHandshakeMessageV1 implements InitiatorHandshakeMessage { - public static final int MESSAGE_LENGTH = - ECIESHandshaker.SIGNATURE_LENGTH - + ECIESHandshaker.HASH_EPH_PUBKEY_LENGTH - + ECIESHandshaker.PUBKEY_LENGTH - + ECIESHandshaker.NONCE_LENGTH - + ECIESHandshaker.TOKEN_FLAG_LENGTH; - - private final SECPPublicKey pubKey; - private final SECPSignature signature; - private final SECPPublicKey ephPubKey; - private final Bytes32 ephPubKeyHash; - private final Bytes32 nonce; - private final boolean token; - - private static final SignatureAlgorithm SIGNATURE_ALGORITHM = - SignatureAlgorithmFactory.getInstance(); - - InitiatorHandshakeMessageV1( - final SECPPublicKey pubKey, - final SECPSignature signature, - final SECPPublicKey ephPubKey, - final Bytes32 ephPubKeyHash, - final Bytes32 nonce, - final boolean token) { - this.pubKey = pubKey; - this.signature = signature; - this.ephPubKey = ephPubKey; - this.ephPubKeyHash = ephPubKeyHash; - this.nonce = nonce; - this.token = token; - } - - public static InitiatorHandshakeMessageV1 create( - final SECPPublicKey ourPubKey, - final KeyPair ephKeyPair, - final Bytes32 staticSharedSecret, - final Bytes32 nonce, - final boolean token) { - final Bytes32 ephPubKeyHash = Hash.keccak256(ephKeyPair.getPublicKey().getEncodedBytes()); - - // XOR of the static shared secret and the generated nonce. - final SECPSignature signature = - SIGNATURE_ALGORITHM.sign(staticSharedSecret.xor(nonce), ephKeyPair); - return new InitiatorHandshakeMessageV1( - ourPubKey, signature, ephKeyPair.getPublicKey(), ephPubKeyHash, nonce, token); - } - - /** - * Decodes this message. - * - * @param bytes The raw bytes. - * @param nodeKey The nodeKey used to calculate ECDH key agreements. - * @return The decoded message. - */ - public static InitiatorHandshakeMessageV1 decode(final Bytes bytes, final NodeKey nodeKey) { - checkState(bytes.size() == MESSAGE_LENGTH); - - int offset = 0; - final SECPSignature signature = - SIGNATURE_ALGORITHM.decodeSignature(bytes.slice(offset, ECIESHandshaker.SIGNATURE_LENGTH)); - final Bytes32 ephPubKeyHash = - Bytes32.wrap( - bytes.slice( - offset += ECIESHandshaker.SIGNATURE_LENGTH, ECIESHandshaker.HASH_EPH_PUBKEY_LENGTH), - 0); - final SECPPublicKey pubKey = - SIGNATURE_ALGORITHM.createPublicKey( - bytes.slice( - offset += ECIESHandshaker.HASH_EPH_PUBKEY_LENGTH, ECIESHandshaker.PUBKEY_LENGTH)); - final Bytes32 nonce = - Bytes32.wrap( - bytes.slice(offset += ECIESHandshaker.PUBKEY_LENGTH, ECIESHandshaker.NONCE_LENGTH), 0); - final boolean token = bytes.get(offset) == 0x01; - - final Bytes32 staticSharedSecret = nodeKey.calculateECDHKeyAgreement(pubKey); - final SECPPublicKey ephPubKey = - SIGNATURE_ALGORITHM - .recoverPublicKeyFromSignature(staticSharedSecret.xor(nonce), signature) - .orElseThrow(() -> new RuntimeException("Could not recover public key from signature")); - - return new InitiatorHandshakeMessageV1( - pubKey, signature, ephPubKey, ephPubKeyHash, nonce, token); - } - - @Override - public Bytes encode() { - final MutableBytes bytes = MutableBytes.create(MESSAGE_LENGTH); - signature.encodedBytes().copyTo(bytes, 0); - ephPubKeyHash.copyTo(bytes, ECIESHandshaker.SIGNATURE_LENGTH); - pubKey - .getEncodedBytes() - .copyTo(bytes, ECIESHandshaker.SIGNATURE_LENGTH + ECIESHandshaker.HASH_EPH_PUBKEY_LENGTH); - nonce.copyTo( - bytes, - ECIESHandshaker.SIGNATURE_LENGTH - + ECIESHandshaker.HASH_EPH_PUBKEY_LENGTH - + ECIESHandshaker.PUBKEY_LENGTH); - bytes.set(MESSAGE_LENGTH - 1, (byte) (token ? 0x01 : 0x00)); - return bytes; - } - - @Override - public SECPPublicKey getPubKey() { - return pubKey; - } - - @Override - public Bytes32 getEphPubKeyHash() { - return ephPubKeyHash; - } - - @Override - public Bytes32 getNonce() { - return nonce; - } - - @Override - public SECPPublicKey getEphPubKey() { - return ephPubKey; - } - - @Override - public String toString() { - return "InitiatorHandshakeMessage{" - + "pubKey=" - + pubKey - + ", signature=" - + signature - + ", ephPubKey=" - + ephPubKey - + ", ephPubKeyHash=" - + ephPubKeyHash - + ", nonce=" - + nonce - + ", token=" - + token - + '}'; - } -} diff --git a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/handshake/ecies/ResponderHandshakeMessageV1.java b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/handshake/ecies/ResponderHandshakeMessageV1.java deleted file mode 100644 index e176272b66d..00000000000 --- a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/handshake/ecies/ResponderHandshakeMessageV1.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright ConsenSys AG. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ -package org.hyperledger.besu.ethereum.p2p.rlpx.handshake.ecies; - -import static com.google.common.base.Preconditions.checkArgument; - -import org.hyperledger.besu.crypto.SECPPublicKey; -import org.hyperledger.besu.crypto.SignatureAlgorithmFactory; - -import org.apache.tuweni.bytes.Bytes; -import org.apache.tuweni.bytes.Bytes32; -import org.apache.tuweni.bytes.MutableBytes; - -/** - * The responder's handshake message. - * - *

This message must be sent by the party who responded to the RLPX connection, in response to - * the initiator message. - * - *

Message structure

- * - * The following describes the message structure: - * - *
- *   authRecipient -> E(remote-pubk,
- *                      remote-ephemeral-pubk
- *                      || nonce
- *                      || 0x0)
- * 
- * - * @see Structure of the - * responder response - */ -public final class ResponderHandshakeMessageV1 implements ResponderHandshakeMessage { - public static final int MESSAGE_LENGTH = - ECIESHandshaker.PUBKEY_LENGTH - + ECIESHandshaker.NONCE_LENGTH - + ECIESHandshaker.TOKEN_FLAG_LENGTH; - - private final SECPPublicKey ephPublicKey; // 64 bytes - uncompressed and no type byte - private final Bytes32 nonce; // 32 bytes - private final boolean token; // 1 byte - 0x00 or 0x01 - - private ResponderHandshakeMessageV1( - final SECPPublicKey ephPublicKey, final Bytes32 nonce, final boolean token) { - this.ephPublicKey = ephPublicKey; - this.nonce = nonce; - this.token = token; - } - - public static ResponderHandshakeMessageV1 create( - final SECPPublicKey ephPublicKey, final Bytes32 nonce, final boolean token) { - return new ResponderHandshakeMessageV1(ephPublicKey, nonce, token); - } - - public static ResponderHandshakeMessageV1 decode(final Bytes bytes) { - checkArgument(bytes.size() == MESSAGE_LENGTH); - - final Bytes pubk = bytes.slice(0, ECIESHandshaker.PUBKEY_LENGTH); - final SECPPublicKey ephPubKey = SignatureAlgorithmFactory.getInstance().createPublicKey(pubk); - final Bytes32 nonce = - Bytes32.wrap(bytes.slice(ECIESHandshaker.PUBKEY_LENGTH, ECIESHandshaker.NONCE_LENGTH), 0); - final boolean token = - bytes.get(ECIESHandshaker.PUBKEY_LENGTH + ECIESHandshaker.NONCE_LENGTH) == 0x01; - return new ResponderHandshakeMessageV1(ephPubKey, nonce, token); - } - - @Override - public Bytes encode() { - final MutableBytes bytes = MutableBytes.create(MESSAGE_LENGTH); - ephPublicKey.getEncodedBytes().copyTo(bytes, 0); - nonce.copyTo(bytes, ECIESHandshaker.PUBKEY_LENGTH); - Bytes.of((byte) (token ? 0x01 : 0x00)) - .copyTo(bytes, ECIESHandshaker.PUBKEY_LENGTH + ECIESHandshaker.NONCE_LENGTH); - return bytes; - } - - @Override - public SECPPublicKey getEphPublicKey() { - return ephPublicKey; - } - - @Override - public Bytes32 getNonce() { - return nonce; - } - - @Override - public String toString() { - return "ResponderHandshakeMessage{" - + "ephPublicKey=" - + ephPublicKey - + ", nonce=" - + nonce - + ", token=" - + token - + '}'; - } -} diff --git a/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/rlpx/connections/netty/DeFramerTest.java b/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/rlpx/connections/netty/DeFramerTest.java index b6c9af59891..571a17008d5 100644 --- a/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/rlpx/connections/netty/DeFramerTest.java +++ b/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/rlpx/connections/netty/DeFramerTest.java @@ -112,7 +112,8 @@ public class DeFramerTest { private final LocalNode localNode = LocalNode.create(clientId, p2pVersion, capabilities, localEnode); - private final DeFramer deFramer = createDeFramer(null, Optional.empty()); + private final Bytes authenticatedNodeId = Peer.randomId(); + private final DeFramer deFramer = createDeFramer(null, Optional.empty(), authenticatedNodeId); @BeforeEach @SuppressWarnings("unchecked") @@ -183,6 +184,7 @@ public void decode_handlesHello() throws ExecutionException, InterruptedExceptio final Peer peer = createRemotePeer(); final PeerInfo remotePeerInfo = createPeerInfo(peer); + final DeFramer deFramer = createDeFramer(null, Optional.empty(), peer.getId()); final HelloMessage helloMessage = HelloMessage.create(remotePeerInfo); final ByteBuf data = Unpooled.wrappedBuffer(helloMessage.getData().toArray()); @@ -229,7 +231,7 @@ public void decode_handlesHelloFromPeerWithAdvertisedPortOf0() final Peer peer = createRemotePeer(); final PeerInfo remotePeerInfo = new PeerInfo(p2pVersion, clientId, capabilities, 0, peer.getId()); - final DeFramer deFramer = createDeFramer(null, Optional.empty()); + final DeFramer deFramer = createDeFramer(null, Optional.empty(), peer.getId()); final HelloMessage helloMessage = HelloMessage.create(remotePeerInfo); final ByteBuf data = Unpooled.wrappedBuffer(helloMessage.getData().toArray()); @@ -288,7 +290,7 @@ public void decode_duringHandshakeFindsPeerInPeerTable() DiscoveryPeerV4.from(DefaultPeer.fromURI(enodeURLString)); final ForkId forkId = new ForkId(Bytes.fromHexString("0x190a55ad"), 4L); discoveryPeer.orElseThrow().setForkId(forkId); - final DeFramer deFramer = createDeFramer(null, discoveryPeer); + final DeFramer deFramer = createDeFramer(null, discoveryPeer, peer.getId()); final ByteBuf data = Unpooled.wrappedBuffer(helloMessage.getData().toArray()); when(framer.deframe(eq(data))) .thenReturn(new RawMessage(helloMessage.getCode(), helloMessage.getData())) @@ -304,20 +306,15 @@ public void decode_duringHandshakeFindsPeerInPeerTable() } @Test - public void decode_handlesUnexpectedPeerId() { + public void decode_handlesHelloNodeIdMismatchWithHandshake() { final ChannelFuture future = NettyMocks.channelFuture(false); when(channel.closeFuture()).thenReturn(future); - final Peer peer = createRemotePeer(); + final Bytes authenticatedId = Peer.randomId(); final Bytes mismatchedId = Peer.randomId(); final PeerInfo remotePeerInfo = - new PeerInfo( - p2pVersion, - clientId, - capabilities, - peer.getEnodeURL().getListeningPortOrZero(), - mismatchedId); - final DeFramer deFramer = createDeFramer(peer, Optional.empty()); + new PeerInfo(p2pVersion, clientId, capabilities, 30303, mismatchedId); + final DeFramer deFramer = createDeFramer(null, Optional.empty(), authenticatedId); final HelloMessage helloMessage = HelloMessage.create(remotePeerInfo); final ByteBuf data = Unpooled.wrappedBuffer(helloMessage.getData().toArray()); @@ -331,12 +328,10 @@ public void decode_handlesUnexpectedPeerId() { assertThat(connectFuture).isCompletedExceptionally(); assertThatThrownBy(connectFuture::get) .hasCauseInstanceOf(UnexpectedPeerConnectionException.class) - .hasMessageContaining("Expected id " + peer.getId().toString()); + .hasMessageContaining("Hello nodeId does not match handshake identity"); assertThat(out).isEmpty(); - - // Next phase of pipeline should be setup - verify(pipeline, times(1)).addLast(any(ChannelHandler[].class)); + verify(ctx).close(); } @Test @@ -344,13 +339,11 @@ public void decode_handlesNoSharedCaps() { final ChannelFuture future = NettyMocks.channelFuture(false); when(channel.closeFuture()).thenReturn(future); + final Bytes nodeId = Peer.randomId(); final PeerInfo remotePeerInfo = new PeerInfo( - p2pVersion, - "bla", - Arrays.asList(Capability.create("eth", 254)), - 30303, - Peer.randomId()); + p2pVersion, "bla", Arrays.asList(Capability.create("eth", 254)), 30303, nodeId); + final DeFramer deFramer = createDeFramer(null, Optional.empty(), nodeId); final HelloMessage helloMessage = HelloMessage.create(remotePeerInfo); final ByteBuf data = Unpooled.wrappedBuffer(helloMessage.getData().toArray()); when(framer.deframe(eq(data))) @@ -392,6 +385,7 @@ public void decode_shouldHandleRemoteSocketAddressIsNull() { final Peer peer = createRemotePeer(); final PeerInfo remotePeerInfo = new PeerInfo(p2pVersion, clientId, capabilities, 0, peer.getId()); + final DeFramer deFramer = createDeFramer(null, Optional.empty(), peer.getId()); final HelloMessage helloMessage = HelloMessage.create(remotePeerInfo); final ByteBuf data = Unpooled.wrappedBuffer(helloMessage.getData().toArray()); when(framer.deframe(any())) @@ -458,7 +452,9 @@ private PeerInfo createPeerInfo(final Peer forPeer) { } private DeFramer createDeFramer( - final Peer expectedPeer, final Optional peerInPeerTable) { + final Peer expectedPeer, + final Optional peerInPeerTable, + final Bytes nodeId) { final PeerTable peerTable = new PeerTable(localNode.getPeerInfo().getNodeId()); PeerLookup peerLookup = new PeerLookup(); peerLookup.set(peer -> peerTable.get(peer).map(p -> p)); @@ -473,6 +469,7 @@ private DeFramer createDeFramer( new NoOpMetricsSystem(), true, peerLookup, - RlpxConfiguration.DEFAULT_MAX_MESSAGE_SIZE); + RlpxConfiguration.DEFAULT_MAX_MESSAGE_SIZE, + nodeId); } } diff --git a/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/rlpx/handshake/ecies/ECIESHandshakeTest.java b/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/rlpx/handshake/ecies/ECIESHandshakeTest.java index dec93e1ccca..360750923c1 100644 --- a/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/rlpx/handshake/ecies/ECIESHandshakeTest.java +++ b/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/rlpx/handshake/ecies/ECIESHandshakeTest.java @@ -25,8 +25,6 @@ import java.util.Optional; import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; -import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.bytes.Bytes32; import org.junit.jupiter.api.Test; @@ -62,39 +60,6 @@ private static class Input { h32("0xcd26fecb93657d1cd9e9eaf4f8be720b56dd1d39f190c4e1c6b7ec66f077bb11"); private static final Bytes32 responderNonce = h32("0xf37ec61d84cea03dcc5e8385db93248584e8af4b4d1c832d8c7453c0089687a7"); - - // PyEVM - private static final byte[] pyEvmInitiatorRqEnc = - h("0x04a0274c5951e32132e7f088c9bdfdc76c9d91f0dc6078e848f8e3361193dbdc43b94351ea3d89e4ff33ddcefbc80070498824857f499656c4f79bbd97b6c51a514251d69fd1785ef8764bd1d262a883f780964cce6a14ff206daf1206aa073a2d35ce2697ebf3514225bef186631b2fd2316a4b7bcdefec8d75a1025ba2c5404a34e7795e1dd4bc01c6113ece07b0df13b69d3ba654a36e35e69ff9d482d88d2f0228e7d96fe11dccbb465a1831c7d4ad3a026924b182fc2bdfe016a6944312021da5cc459713b13b86a686cf34d6fe6615020e4acf26bf0d5b7579ba813e7723eb95b3cef9942f01a58bd61baee7c9bdd438956b426a4ffe238e61746a8c93d5e10680617c82e48d706ac4953f5e1c4c4f7d013c87d34a06626f498f34576dc017fdd3d581e83cfd26cf125b6d2bda1f1d56") - .toArray(); - } - - // Messages. - private static class Messages { - - // Encrypted messages -- we cannot assert against these because the Integrated Encryption Scheme - // is designed to - // use a different ephemeral key everytime. - private static final byte[] initiatorMsgEnc = - h("0x04a0274c5951e32132e7f088c9bdfdc76c9d91f0dc6078e848f8e3361193dbdc43b94351ea3d89e4ff33ddcefbc80070498824857f499656c4f79bbd97b6c51a514251d69fd1785ef8764bd1d262a883f780964cce6a14ff206daf1206aa073a2d35ce2697ebf3514225bef186631b2fd2316a4b7bcdefec8d75a1025ba2c5404a34e7795e1dd4bc01c6113ece07b0df13b69d3ba654a36e35e69ff9d482d88d2f0228e7d96fe11dccbb465a1831c7d4ad3a026924b182fc2bdfe016a6944312021da5cc459713b13b86a686cf34d6fe6615020e4acf26bf0d5b7579ba813e7723eb95b3cef9942f01a58bd61baee7c9bdd438956b426a4ffe238e61746a8c93d5e10680617c82e48d706ac4953f5e1c4c4f7d013c87d34a06626f498f34576dc017fdd3d581e83cfd26cf125b6d2bda1f1d56") - .toArray(); - private static final byte[] responderMsgEnc = - h("0x049934a7b2d7f9af8fd9db941d9da281ac9381b5740e1f64f7092f3588d4f87f5ce55191a6653e5e80c1c5dd538169aa123e70dc6ffc5af1827e546c0e958e42dad355bcc1fcb9cdf2cf47ff524d2ad98cbf275e661bf4cf00960e74b5956b799771334f426df007350b46049adb21a6e78ab1408d5e6ccde6fb5e69f0f4c92bb9c725c02f99fa72b9cdc8dd53cff089e0e73317f61cc5abf6152513cb7d833f09d2851603919bf0fbe44d79a09245c6e8338eb502083dc84b846f2fee1cc310d2cc8b1b9334728f97220bb799376233e113") - .toArray(); - } - - private static class Expectations { - - private static final byte[] aesSecret = - h32("0xc0458fa97a5230830e05f4f20b7c755c1d4e54b1ce5cf43260bb191eef4e418d").toArray(); - private static final byte[] macSecret = - h32("0x48c938884d5067a1598272fcddaa4b833cd5e7d92e8228c0ecdfabbe68aef7f1").toArray(); - private static final byte[] token = - h32("0x3f9ec2592d1554852b1f54d228f042ed0a9310ea86d038dc2b401ba8cd7fdac4").toArray(); - private static final byte[] initialEgressMac = - h32("0x09771e93b1a6109e97074cbe2d2b0cf3d3878efafe68f53c41bb60c0ec49097e").toArray(); - private static final byte[] initialIngressMac = - h32("0x75823d96e23136c89666ee025fb21a432be906512b3dd4a3049e898adb433847").toArray(); } @Test @@ -146,71 +111,6 @@ public void authPlainTextWithEncryption() { assertThat(initiator.getResponderNonce()).isEqualTo(Input.responderNonce); } - @Test - public void encryptedInitiationRqFromPyEvm() { - // Create the responder handshaker. - final ECIESHandshaker responder = new ECIESHandshaker(); - - // Prepare the handshaker with the responder's keypair. - responder.prepareResponder(NodeKeyUtils.createFrom(Input.responderKeyPair)); - - // Set the test data. - responder.setEphKeyPair(Input.responderEphKeyPair); - responder.setResponderNonce(Input.responderNonce); - - // Pusht the request taken from PyEVM. - final Optional responderMsg = - responder.handleMessage(Unpooled.wrappedBuffer(Input.pyEvmInitiatorRqEnc)); - assertThat(responderMsg).isPresent(); - assertThat(responder.getStatus()).isEqualTo(HandshakeStatus.SUCCESS); - } - - @Test - public void ingressEgressMacsAsExpected() { - // Initiator end of the handshake. - final ECIESHandshaker initiator = new ECIESHandshaker(); - initiator.prepareInitiator( - NodeKeyUtils.createFrom(Input.initiatorKeyPair), Input.responderKeyPair.getPublicKey()); - initiator.firstMessage(); - initiator.setInitiatorMsgEnc(Bytes.wrap(Messages.initiatorMsgEnc)); - initiator.setEphKeyPair(Input.initiatorEphKeyPair); - initiator.setInitiatorNonce(Input.initiatorNonce); - - // Responder end of the handshake. - final ECIESHandshaker responder = new ECIESHandshaker(); - responder.prepareResponder(NodeKeyUtils.createFrom(Input.responderKeyPair)); - responder.setEphKeyPair(Input.responderEphKeyPair); - responder.setResponderNonce(Input.responderNonce); - - // Exchange encrypted test messages. - responder.handleMessage(Unpooled.wrappedBuffer(Messages.initiatorMsgEnc)); - initiator.handleMessage(Unpooled.wrappedBuffer(Messages.responderMsgEnc)); - - // Override the message sent from the responder with the test vector, and regenerate the - // secrets. - responder.setResponderMsgEnc(Bytes.wrap(Messages.responderMsgEnc)); - responder.computeSecrets(); - - // Assert that the initiator's secrets match the expected values. - assertThat(initiator.secrets().getAesSecret()).isEqualTo(Expectations.aesSecret); - assertThat(initiator.secrets().getMacSecret()).isEqualTo(Expectations.macSecret); - assertThat(initiator.secrets().getToken()).isEqualTo(Expectations.token); - assertThat(initiator.secrets().getIngressMac()).isEqualTo(Expectations.initialIngressMac); - assertThat(initiator.secrets().getEgressMac()).isEqualTo(Expectations.initialEgressMac); - - // Assert that the responder's secrets match the expected values, where the ingress and egress - // are reversed. - assertThat(responder.secrets().getAesSecret()).isEqualTo(Expectations.aesSecret); - assertThat(responder.secrets().getMacSecret()).isEqualTo(Expectations.macSecret); - assertThat(responder.secrets().getToken()).isEqualTo(Expectations.token); - assertThat(responder.secrets().getIngressMac()).isEqualTo(Expectations.initialEgressMac); - assertThat(responder.secrets().getEgressMac()).isEqualTo(Expectations.initialIngressMac); - } - - private static Bytes h(final String hex) { - return Bytes.fromHexString(hex); - } - private static Bytes32 h32(final String hex) { return Bytes32.fromHexString(hex); }