Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,14 @@ jobs:
- name: Test
working-directory: ${{ env.WORKING_DIR }}
env:
TEST_OPTS: -c ${{ env.BUILD_CONFIG }} --no-build
TEST_OPTS: -c ${{ env.BUILD_CONFIG }} --no-build -- --minimum-expected-tests 1
run: |
dotnet test Libp2p.Core.Tests/Libp2p.Core.Tests.csproj ${{ env.TEST_OPTS }}
dotnet test Libp2p.Protocols.Multistream.Tests/Libp2p.Protocols.Multistream.Tests.csproj ${{ env.TEST_OPTS }}
dotnet test Libp2p.Protocols.Noise.Tests/Libp2p.Protocols.Noise.Tests.csproj ${{ env.TEST_OPTS }}
dotnet test Libp2p.Protocols.Pubsub.Tests/Libp2p.Protocols.Pubsub.Tests.csproj ${{ env.TEST_OPTS }}
dotnet test Libp2p.Protocols.Quic.Tests/Libp2p.Protocols.Quic.Tests.csproj ${{ env.TEST_OPTS }}
dotnet test Libp2p.Protocols.Tls.Tests/Libp2p.Protocols.Tls.Tests.csproj ${{ env.TEST_OPTS }}
dotnet test Libp2p.Protocols.Yamux.Tests/Libp2p.Protocols.Yamux.Tests.csproj ${{ env.TEST_OPTS }}
dotnet test Libp2p.E2eTests/Libp2p.E2eTests.csproj ${{ env.TEST_OPTS }}
dotnet test Libp2p.Protocols.Pubsub.E2eTests/Libp2p.Protocols.Pubsub.E2eTests.csproj ${{ env.TEST_OPTS }}
Expand Down
23 changes: 23 additions & 0 deletions src/libp2p/Libp2p.Core.Tests/IdentityTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,27 @@ public void Test_Signing(KeyType keyType)

Assert.That(id.VerifySignature(message, signature), Is.True);
}

[Test]
public void Test_GeneratedSecp256K1KeysAlwaysSign()
{
byte[] message = [1, 2, 3];
for (int i = 0; i < 1000; i++)
{
Identity id = new(keyType: KeyType.Secp256K1);
Assert.That(id.VerifySignature(message, id.Sign(message)), Is.True, $"Generated key {i}");
}
}

[Test]
public void Test_ImportedSecp256K1PrivateKeyIsUnsigned()
{
byte[] privateKey = new byte[32];
privateKey[0] = 0x80;
privateKey[^1] = 1;
Identity id = new(privateKey, KeyType.Secp256K1);
byte[] message = [1, 2, 3];

Assert.That(id.VerifySignature(message, id.Sign(message)), Is.True);
}
}
6 changes: 4 additions & 2 deletions src/libp2p/Libp2p.Core/Identity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,10 @@ public Identity(PrivateKey privateKey)
ECKeyPairGenerator generator = new("ECDSA");
generator.Init(keyParams);
AsymmetricCipherKeyPair keyPair = generator.GenerateKeyPair();
byte[] privateKeyBytes = ((ECPrivateKeyParameters)keyPair.Private).D.ToByteArrayUnsigned();
Span<byte> privateKeySpan = stackalloc byte[32];
((ECPrivateKeyParameters)keyPair.Private).D.ToByteArrayUnsigned(privateKeySpan);
privateKeySpan.Clear();
privateKeyBytes.CopyTo(privateKeySpan[^privateKeyBytes.Length..]);
privateKeyData = ByteString.CopyFrom(privateKeySpan);
publicKeyData = ByteString.CopyFrom(((ECPublicKeyParameters)keyPair.Public).Q.GetEncoded(true));
}
Expand Down Expand Up @@ -128,7 +130,7 @@ private static PublicKey GetPublicKey(PrivateKey privateKey)
case KeyType.Secp256K1:
{
X9ECParameters curve = CustomNamedCurves.GetByName("secp256k1");
ECPoint pointQ = curve.G.Multiply(new BigInteger(privateKey.Data.ToArray()));
ECPoint pointQ = curve.G.Multiply(new BigInteger(1, privateKey.Data.ToArray()));
publicKeyData = ByteString.CopyFrom(pointQ.GetEncoded(true));
}
break;
Expand Down
4 changes: 4 additions & 0 deletions src/libp2p/Libp2p.Protocols.IpTcp/IpTcpProtocol.cs
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,10 @@ public async Task DialAsync(ITransportContext context, Multiaddress remoteAddr,

INewConnectionContext connectionCtx = context.CreateConnection();
connectionCtx.State.RemoteAddress = client.RemoteEndPoint.ToMultiaddress(ProtocolType.Tcp);
if (remoteAddr.Get<P2P>() is { } requestedPeerId)
{
connectionCtx.State.RemoteAddress.Add(requestedPeerId);
Comment thread
flcl42 marked this conversation as resolved.
}
connectionCtx.State.LocalAddress = client.LocalEndPoint.ToMultiaddress(ProtocolType.Tcp);

connectionCtx.Token.Register(client.Close);
Expand Down
15 changes: 15 additions & 0 deletions src/libp2p/Libp2p.Protocols.Noise.Tests/NoiseProtocolTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Buffers;
using System.Buffers.Binary;
using System.Reflection;
using System.Text;
using Google.Protobuf;
using Microsoft.Extensions.Logging;
Expand All @@ -20,6 +21,20 @@ namespace Nethermind.Libp2p.Protocols.Noise.Tests;
[Parallelizable(scope: ParallelScope.All)]
public class NoiseProtocolTests
{
[Test]
public void Test_RemoteIdentityRejectsUnexpectedDialedPeer()
{
IConnectionContext context = Substitute.For<IConnectionContext>();
context.State.Returns(new State { RemoteAddress = $"/ip4/127.0.0.1/tcp/0/p2p/{TestPeers.PeerId(3)}" });

MethodInfo setRemoteIdentity = typeof(NoiseProtocol).GetMethod("SetRemoteIdentity", BindingFlags.Static | BindingFlags.NonPublic)!;

TargetInvocationException? exception = Assert.Throws<TargetInvocationException>(() =>
setRemoteIdentity.Invoke(null, [context, TestPeers.Identity(2).PublicKey]));

Assert.That(exception!.InnerException, Is.TypeOf<Libp2pException>());
}

[Test]
public async Task Test_ConnectionEstablished_AfterHandshake()
{
Expand Down
38 changes: 23 additions & 15 deletions src/libp2p/Libp2p.Protocols.Noise/NoiseProtocol.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,6 @@ public async Task DialAsync(IChannel downChannel, IConnectionContext context)
throw new Libp2pException("Noise handshake signature verification failed: responder identity key does not match noise static key.");
}

context.State.RemotePublicKey = msg1KeyDecoded;


List<string> responderMuxers = msg1Decoded.Extensions?.StreamMuxers?
.Where(m => !string.IsNullOrEmpty(m))
.ToList() ?? [];
Expand All @@ -115,11 +112,7 @@ public async Task DialAsync(IChannel downChannel, IConnectionContext context)
};
}

PeerId remotePeerId = new(msg1KeyDecoded);
if (!context.State.RemoteAddress.Has<P2P>())
{
context.State.RemoteAddress.Add(new P2P(remotePeerId.ToString()));
}
SetRemoteIdentity(context, msg1KeyDecoded);

byte[] msg = [.. Encoding.UTF8.GetBytes(PayloadSigPrefix), .. ByteString.CopyFrom(clientStatic.PublicKey)];
byte[] sig = context.Peer.Identity.Sign(msg);
Expand Down Expand Up @@ -226,8 +219,6 @@ public async Task ListenAsync(IChannel downChannel, IConnectionContext context)
throw new Libp2pException("Noise handshake signature verification failed: initiator identity key does not match noise static key.");
}

context.State.RemotePublicKey = msg2KeyDecoded;

Transport? transport = msg2.Transport;

List<string> initiatorMuxers = msg2Decoded.Extensions?.StreamMuxers?.Where(m => !string.IsNullOrEmpty(m)).ToList() ?? [];
Expand All @@ -245,11 +236,7 @@ public async Task ListenAsync(IChannel downChannel, IConnectionContext context)
};
}

if (!context.State.RemoteAddress.Has<P2P>())
{
PeerId remotePeerId = new(msg2KeyDecoded);
context.State.RemoteAddress.Add(new P2P(remotePeerId.ToString()));
}
SetRemoteIdentity(context, msg2KeyDecoded);

_logger?.LogDebug("Established connection to {peer}", context.State.RemoteAddress);

Expand All @@ -262,6 +249,27 @@ public async Task ListenAsync(IChannel downChannel, IConnectionContext context)
_logger?.LogDebug("Closed");
}

private static void SetRemoteIdentity(IConnectionContext context, PublicKey remotePublicKey)
{
if (context.State.RemotePublicKey is { } existingRemotePublicKey && existingRemotePublicKey.ToByteString() != remotePublicKey.ToByteString())
{
throw new Libp2pException("Noise identity does not match the previously authenticated remote public key.");
}

PeerId remotePeerId = new(remotePublicKey);
PeerId? expectedPeerId = context.State.RemoteAddress?.GetPeerId();
if (expectedPeerId is not null && expectedPeerId != remotePeerId)
{
throw new Libp2pException("Noise handshake identity does not match the expected remote peer ID.");
}

context.State.RemotePublicKey = remotePublicKey;
if (context.State.RemoteAddress is { } remoteAddress && expectedPeerId is null)
{
remoteAddress.Add(new P2P(remotePeerId.ToString()));
}
}

private static Task ExchangeData(Transport transport, IChannel downChannel, IChannel upChannel, ILogger? logger)
{
// UP -> DOWN
Expand Down
55 changes: 55 additions & 0 deletions src/libp2p/Libp2p.Protocols.Tls.Tests/IpTcpPeerIdentityTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited
// SPDX-License-Identifier: MIT

using Multiformats.Address;
using Nethermind.Libp2p.Core;
using Nethermind.Libp2p.Core.TestsBase;
using NSubstitute;
using System.Net;
using System.Net.Sockets;

namespace Nethermind.Libp2p.Protocols.TLS.Tests;

[TestFixture]
public class IpTcpPeerIdentityTests
{
[TestCase(false)]
[TestCase(true)]
public async Task Test_DialPassesExpectedPeerIdToSecurityProtocol(bool includePeerId)
{
using TcpListener listener = new(IPAddress.Loopback, 0);
listener.Start();
int port = ((IPEndPoint)listener.LocalEndpoint).Port;
Multiaddress dialAddress = includePeerId
? $"/ip4/127.0.0.1/tcp/{port}/p2p/{TestPeers.PeerId(2)}"
: $"/ip4/127.0.0.1/tcp/{port}";

State state = new();
TestChannel channel = new();
TaskCompletionSource<string> upgradedAddress = new(TaskCreationOptions.RunContinuationsAsynchronously);
INewConnectionContext connection = Substitute.For<INewConnectionContext>();
connection.State.Returns(state);
connection.Upgrade(Arg.Any<UpgradeOptions>()).Returns(_ =>
{
// Capture the address before a security protocol can enrich it.
upgradedAddress.SetResult(state.RemoteAddress!.ToString());
return channel;
});
ITransportContext context = Substitute.For<ITransportContext>();
context.CreateConnection().Returns(connection);

using CancellationTokenSource cancellation = new(TimeSpan.FromSeconds(15));
Task dialTask = new IpTcpProtocol().DialAsync(context, dialAddress, cancellation.Token);
try
{
using TcpClient accepted = await listener.AcceptTcpClientAsync(cancellation.Token);
string actualAddress = await upgradedAddress.Task.WaitAsync(cancellation.Token);
Assert.That(actualAddress, Is.EqualTo(dialAddress.ToString()));
}
finally
{
await channel.CloseAsync();
await dialTask.WaitAsync(TimeSpan.FromSeconds(15));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
</ItemGroup>

<ItemGroup>
<ProjectReference Include="../Libp2p.Protocols.IpTcp/Libp2p.Protocols.IpTcp.csproj" />
<ProjectReference Include="../Libp2p.Protocols.Tls/Libp2p.Protocols.Tls.csproj" />
<ProjectReference Include="..\Libp2p.Core.TestsBase\Libp2p.Core.TestsBase.csproj" />
</ItemGroup>
Expand Down
104 changes: 90 additions & 14 deletions src/libp2p/Libp2p.Protocols.Tls.Tests/TlsProtocolTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
using Microsoft.Extensions.Logging;
using Multiformats.Address;
using Nethermind.Libp2p.Core;
using Nethermind.Libp2p.Core.Exceptions;
using Nethermind.Libp2p.Core.TestsBase;
using Nethermind.Libp2p.Protocols.Quic;
using Nethermind.Libp2p.Protocols.Tls;
using NSubstitute;
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Reflection;
using System.Security.Authentication;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
Expand All @@ -22,8 +24,9 @@ namespace Nethermind.Libp2p.Protocols.TLS.Tests;
[Parallelizable(scope: ParallelScope.All)]
public class TlsProtocolTests
{
[Test]
public async Task Test_ConnectionEstablished_AfterHandshake()
[TestCase(false)]
[TestCase(true)]
public async Task Test_ConnectionEstablished_AfterHandshake(bool includePeerId)
{
// Arrange
IChannel downChannel = new TestChannel();
Expand All @@ -32,21 +35,34 @@ public async Task Test_ConnectionEstablished_AfterHandshake()

TestChannel upChannel = new();
TestChannel listenerUpChannel = new();
TaskCompletionSource dialerUpgraded = new(TaskCreationOptions.RunContinuationsAsynchronously);
TaskCompletionSource listenerUpgraded = new(TaskCreationOptions.RunContinuationsAsynchronously);

// Dialer context (identity 1 dials to identity 2)
IConnectionContext dialerContext = Substitute.For<IConnectionContext>();
dialerContext.Peer.Identity.Returns(TestPeers.Identity(1));
dialerContext.Peer.ListenAddresses.Returns([(Multiaddress)$"/ip4/127.0.0.1/tcp/0/p2p/{TestPeers.PeerId(1)}"]);
dialerContext.State.Returns(new State { RemoteAddress = $"/p2p/{TestPeers.PeerId(2)}" });
Multiaddress dialAddress = includePeerId
? $"/ip4/127.0.0.1/tcp/0/p2p/{TestPeers.PeerId(2)}"
: "/ip4/127.0.0.1/tcp/0";
dialerContext.State.Returns(new State { RemoteAddress = dialAddress });
dialerContext.SubProtocols.Returns(Array.Empty<IProtocol>());
dialerContext.Upgrade(Arg.Any<UpgradeOptions>()).Returns(upChannel);
dialerContext.Upgrade(Arg.Any<UpgradeOptions>()).Returns(_ =>
{
dialerUpgraded.SetResult();
return upChannel;
});

// Listener context (identity 2 listens for identity 1)
IConnectionContext listenerContext = Substitute.For<IConnectionContext>();
listenerContext.Peer.Identity.Returns(TestPeers.Identity(2));
listenerContext.State.Returns(new State { RemoteAddress = $"/p2p/{TestPeers.PeerId(1)}" });
listenerContext.State.Returns(new State { RemoteAddress = "/ip4/127.0.0.1/tcp/0" });
listenerContext.SubProtocols.Returns(Array.Empty<IProtocol>());
listenerContext.Upgrade(Arg.Any<UpgradeOptions>()).Returns(listenerUpChannel);
listenerContext.Upgrade(Arg.Any<UpgradeOptions>()).Returns(_ =>
{
listenerUpgraded.SetResult();
return listenerUpChannel;
});

MultiplexerSettings i_multiplexerSettings = new();
MultiplexerSettings r_multiplexerSettings = new();
Expand All @@ -58,16 +74,76 @@ public async Task Test_ConnectionEstablished_AfterHandshake()
Task dialTask = tlsProtocolInitiator.DialAsync(downChannelFromProtocolPov, dialerContext);

int sent = 42;
ValueTask<IOResult> writeTask = listenerUpChannel.Reverse().WriteVarintAsync(sent);
int received = await upChannel.Reverse().ReadVarintAsync();
await writeTask;

await upChannel.CloseAsync();
await listenerUpChannel.CloseAsync();
await downChannel.CloseAsync();
int received;
try
{
Task upgraded = Task.WhenAll(dialerUpgraded.Task, listenerUpgraded.Task);
// Surface handshake failures before attempting application data exchange.
Task completed = await Task.WhenAny(listenTask, dialTask, upgraded).WaitAsync(TimeSpan.FromSeconds(15));
await completed;
await upgraded.WaitAsync(TimeSpan.FromSeconds(15));

ValueTask<IOResult> writeTask = listenerUpChannel.Reverse().WriteVarintAsync(sent);
received = await upChannel.Reverse().ReadVarintAsync().WaitAsync(TimeSpan.FromSeconds(15));
await writeTask;
}
finally
{
await Task.WhenAll(
upChannel.CloseAsync().AsTask(),
listenerUpChannel.CloseAsync().AsTask(),
downChannel.CloseAsync().AsTask()).WaitAsync(TimeSpan.FromSeconds(15));
await Task.WhenAll(listenTask, dialTask).WaitAsync(TimeSpan.FromSeconds(15));
}

// Assert
Assert.That(received, Is.EqualTo(sent));
Assert.Multiple(() =>
{
Assert.That(received, Is.EqualTo(sent));
Assert.That(new Identity(dialerContext.State.RemotePublicKey!).PeerId, Is.EqualTo(TestPeers.PeerId(2)));
Assert.That(new Identity(listenerContext.State.RemotePublicKey!).PeerId, Is.EqualTo(TestPeers.PeerId(1)));
Assert.That(dialerContext.State.RemoteAddress!.GetPeerId(), Is.EqualTo(TestPeers.PeerId(2)));
Assert.That(dialerContext.State.RemoteAddress!.ToString(), Is.EqualTo($"/ip4/127.0.0.1/tcp/0/p2p/{TestPeers.PeerId(2)}"));
Assert.That(listenerContext.State.RemoteAddress!.GetPeerId(), Is.EqualTo(TestPeers.PeerId(1)));
});
}

[Test]
public void Test_TlsIdentityConflictIsRejected()
{
Identity certificateIdentity = TestPeers.Identity(2);
using ECDsa sessionKey = ECDsa.Create();
using X509Certificate2 certificate = CertificateHelper.CertificateFromIdentity(sessionKey, certificateIdentity);

IConnectionContext context = Substitute.For<IConnectionContext>();
State state = new()
{
RemoteAddress = "/ip4/127.0.0.1/tcp/0",
RemotePublicKey = TestPeers.Identity(3).PublicKey,
};
context.State.Returns(state);

MethodInfo setRemoteIdentity = typeof(TlsProtocol).GetMethod("SetRemoteIdentity", BindingFlags.Static | BindingFlags.NonPublic)!;

TargetInvocationException? exception = Assert.Throws<TargetInvocationException>(() =>
setRemoteIdentity.Invoke(null, [context, certificate]));

Assert.That(exception!.InnerException, Is.TypeOf<Libp2pException>());
Assert.That(exception.InnerException!.Message, Does.Contain("does not match"));
}

[Test]
public void Test_TlsCertificateRejectsUnexpectedDialedPeer()
{
Identity certificateIdentity = TestPeers.Identity(2);
using ECDsa sessionKey = ECDsa.Create();
using X509Certificate2 certificate = CertificateHelper.CertificateFromIdentity(sessionKey, certificateIdentity);
Multiaddress requestedAddress = $"/ip4/127.0.0.1/tcp/0/p2p/{TestPeers.PeerId(3)}";

MethodInfo verifyRemoteCertificate = typeof(TlsProtocol).GetMethod("VerifyRemoteCertificate", BindingFlags.Static | BindingFlags.NonPublic)!;
bool isValid = (bool)verifyRemoteCertificate.Invoke(null, [requestedAddress, certificate])!;

Assert.That(isValid, Is.False);
}

[Test]
Expand Down
Loading
Loading