From 56bd2e63b41ae87faadf8392e386b0f2a702a7ff Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Mon, 24 Aug 2026 09:37:35 +0300 Subject: [PATCH 01/10] Update SIPSorcery security dependency --- src/libp2p/Directory.Packages.props | 6 +- .../WebRtcDirectProtocolTests.cs | 16 +++++ .../WebRtcDirectProtocol.cs | 8 ++- .../perf-benchmarks/PerfBenchmarks.csproj | 2 +- src/samples/pubsub-chat/PubsubChat.csproj | 4 +- .../transport-interop/TransportInterop.csproj | 2 +- .../transport-interop/packages.lock.json | 58 ++++++++++--------- 7 files changed, 59 insertions(+), 37 deletions(-) create mode 100644 src/libp2p/Libp2p.Protocols.WebRtc.Tests/WebRtcDirectProtocolTests.cs diff --git a/src/libp2p/Directory.Packages.props b/src/libp2p/Directory.Packages.props index 114c6a5f..6a56f52a 100644 --- a/src/libp2p/Directory.Packages.props +++ b/src/libp2p/Directory.Packages.props @@ -19,9 +19,9 @@ - + - + @@ -38,7 +38,7 @@ - + diff --git a/src/libp2p/Libp2p.Protocols.WebRtc.Tests/WebRtcDirectProtocolTests.cs b/src/libp2p/Libp2p.Protocols.WebRtc.Tests/WebRtcDirectProtocolTests.cs new file mode 100644 index 00000000..a237931f --- /dev/null +++ b/src/libp2p/Libp2p.Protocols.WebRtc.Tests/WebRtcDirectProtocolTests.cs @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: MIT + +namespace Nethermind.Libp2p.Protocols.WebRtc.Tests; + +[TestFixture] +public class WebRtcDirectProtocolTests +{ + [Test] + public void Constructor_CreatesLocalDtlsCertificate() + { + WebRtcDirectProtocol protocol = new(); + + Assert.That(protocol, Is.Not.Null); + } +} diff --git a/src/libp2p/Libp2p.Protocols.WebRtc/WebRtcDirectProtocol.cs b/src/libp2p/Libp2p.Protocols.WebRtc/WebRtcDirectProtocol.cs index 1d4e95f2..a2e5b233 100644 --- a/src/libp2p/Libp2p.Protocols.WebRtc/WebRtcDirectProtocol.cs +++ b/src/libp2p/Libp2p.Protocols.WebRtc/WebRtcDirectProtocol.cs @@ -8,6 +8,8 @@ using Nethermind.Libp2p.Core.Dto; using Nethermind.Libp2p.Protocols.WebRtc.Internals; using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Tls; +using Org.BouncyCastle.Tls.Crypto.Impl.BC; using Org.BouncyCastle.X509; using SIPSorcery.Net; using System.Net; @@ -237,14 +239,16 @@ private RTCPeerConnection CreatePeerConnection() private static (RTCCertificate2 Certificate, DtlsFingerprint Fingerprint) CreateLocalCertificate() { - (X509Certificate certificate, AsymmetricKeyParameter privateKey) = DtlsUtils.CreateSelfSignedEcdsaCert(); + (Certificate certificateChain, AsymmetricKeyParameter privateKey) = + DtlsUtils.CreateSelfSignedTlsCert(new BcTlsCrypto()); + X509Certificate certificate = new(certificateChain.GetCertificateAt(0).GetEncoded()); RTCCertificate2 rtcCertificate = new() { Certificate = certificate, PrivateKey = privateKey, }; - return (rtcCertificate, DtlsFingerprint.FromRtcFingerprint(DtlsUtils.Fingerprint(certificate))); + return (rtcCertificate, DtlsFingerprint.FromRtcFingerprint(DtlsUtils.Fingerprint(certificateChain))); } private async Task ReceiveAnswerAsync( diff --git a/src/samples/perf-benchmarks/PerfBenchmarks.csproj b/src/samples/perf-benchmarks/PerfBenchmarks.csproj index 2f0af588..62679363 100644 --- a/src/samples/perf-benchmarks/PerfBenchmarks.csproj +++ b/src/samples/perf-benchmarks/PerfBenchmarks.csproj @@ -16,7 +16,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/samples/pubsub-chat/PubsubChat.csproj b/src/samples/pubsub-chat/PubsubChat.csproj index a2087c17..b808ff83 100644 --- a/src/samples/pubsub-chat/PubsubChat.csproj +++ b/src/samples/pubsub-chat/PubsubChat.csproj @@ -7,7 +7,7 @@ - + @@ -15,4 +15,4 @@ - \ No newline at end of file + diff --git a/src/samples/transport-interop/TransportInterop.csproj b/src/samples/transport-interop/TransportInterop.csproj index 7bd513e6..dd070183 100644 --- a/src/samples/transport-interop/TransportInterop.csproj +++ b/src/samples/transport-interop/TransportInterop.csproj @@ -14,7 +14,7 @@ - + diff --git a/src/samples/transport-interop/packages.lock.json b/src/samples/transport-interop/packages.lock.json index ccc6fedf..382bbcdb 100644 --- a/src/samples/transport-interop/packages.lock.json +++ b/src/samples/transport-interop/packages.lock.json @@ -24,11 +24,11 @@ }, "Microsoft.Extensions.Logging.Abstractions": { "type": "Direct", - "requested": "[10.0.0, )", - "resolved": "10.0.0", - "contentHash": "FU/IfjDfwaMuKr414SSQNTIti/69bHEMb+QKrskRb26oVqpx3lNFXMjs/RC9ZUuhBhcwDM2BwOgoMw+PZ+beqQ==", + "requested": "[10.0.10, )", + "resolved": "10.0.10", + "contentHash": "zkFxGYUvdxAvIKTyXHrmW+Sux53D4SezD9dMyZ6hrwwzPQJNuwCRy1f5W7AvYTqacEGhWF2XderRQG1OvbV8og==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0" + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10" } }, "Microsoft.Extensions.Logging.Console": { @@ -221,8 +221,8 @@ }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", - "resolved": "10.0.0", - "contentHash": "L3AdmZ1WOK4XXT5YFPEwyt0ep6l8lGIPs7F5OOBZc77Zqeo01Of7XXICy47628sdVl0v/owxYJTe86DTgFwKCA==" + "resolved": "10.0.10", + "contentHash": "z/2xXlFw2aLGjHyEm6E0tQ+In6VfzQzTrtArbQ2c0TQE16ZbyDCMGPvaUT9I0s8rgy9sRWlU2P9waW37qV04qA==" }, "Microsoft.Extensions.Diagnostics": { "type": "Transitive", @@ -471,14 +471,16 @@ }, "SIPSorcery": { "type": "Transitive", - "resolved": "10.0.3", - "contentHash": "+VnD/wvg5myZY2Gj3CXWKdTnfoigv1WyU0uh55pmksT28MBMXBSULLSQt6csNV8VYTJ2aT1CYr9LvBEZrbX6aw==", + "resolved": "10.0.14", + "contentHash": "NvJUoEOpY4NORy1uWdKA70YN8Et3q2IfFtc0JjzhmnVTmFmjOuiqd4LlPMrJ8920+Txm7A5SrhNXwXrOSHO9dQ==", "dependencies": { "BouncyCastle.Cryptography": "2.6.2", "Concentus": "2.2.2", "DnsClient": "1.8.0", + "Makaretu.Dns.Multicast": "0.27.0", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", "SIPSorcery.WebSocketSharp": "0.0.1", - "SIPSorceryMedia.Abstractions": "8.0.12" + "SIPSorceryMedia.Abstractions": "10.0.14" } }, "SIPSorcery.WebSocketSharp": { @@ -488,10 +490,10 @@ }, "SIPSorceryMedia.Abstractions": { "type": "Transitive", - "resolved": "8.0.12", - "contentHash": "70bYl3RFc0lkL6Z2M2GD/vymnWvmnvTSJfEpFHE4Uh+bp5Rz9DWsEvuF8EZEhaW3foPGvpNIfP5nGj52cCXjgw==", + "resolved": "10.0.14", + "contentHash": "sgqwzWcqCS3gQlKLuku4knEoJQX8ySL+awW5+aLqmVpT5fDFi4bkvdCjDF7fQ9y0sh/wM8YYZpueuJ6i+nkJXA==", "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "9.0.0" + "Microsoft.Extensions.Logging.Abstractions": "10.0.10" } }, "StackExchange.Redis": { @@ -572,7 +574,7 @@ "Google.Protobuf": "[3.33.1, )", "Microsoft.Extensions.DependencyInjection": "[10.0.0, )", "Microsoft.Extensions.Hosting": "[9.0.0, )", - "Microsoft.Extensions.Logging.Abstractions": "[10.0.0, )", + "Microsoft.Extensions.Logging.Abstractions": "[10.0.10, )", "Nethermind.Libp2p.Core": "[1.0.0, )", "Nethermind.Libp2p.Protocols.RequestResponse": "[1.0.0, )", "Nethermind.Multiformats.Address": "[1.1.10, )", @@ -611,8 +613,8 @@ "DnsClient": "[1.8.0, )", "Google.Protobuf": "[3.33.1, )", "Microsoft.Extensions.DependencyInjection": "[10.0.0, )", - "Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.0, )", - "Microsoft.Extensions.Logging.Abstractions": "[10.0.0, )", + "Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.10, )", + "Microsoft.Extensions.Logging.Abstractions": "[10.0.10, )", "Nethermind.Multiformats.Address": "[1.1.10, )", "SimpleBase": "[4.0.2, )" } @@ -621,10 +623,10 @@ "type": "Project", "dependencies": { "Certes": "[3.0.4, )", - "Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.0, )", + "Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.10, )", "Microsoft.Extensions.Hosting.Abstractions": "[10.0.0, )", "Microsoft.Extensions.Http": "[10.0.0, )", - "Microsoft.Extensions.Logging.Abstractions": "[10.0.0, )", + "Microsoft.Extensions.Logging.Abstractions": "[10.0.10, )", "Microsoft.Extensions.Options": "[10.0.0, )", "Nethermind.Libp2p.Core": "[1.0.0, )" } @@ -633,7 +635,7 @@ "type": "Project", "dependencies": { "Google.Protobuf": "[3.33.1, )", - "Microsoft.Extensions.Logging.Abstractions": "[10.0.0, )", + "Microsoft.Extensions.Logging.Abstractions": "[10.0.10, )", "Nethermind.Libp2p.Core": "[1.0.0, )", "Nethermind.Libp2p.Protocols.IpTcp": "[1.0.0, )" } @@ -641,7 +643,7 @@ "Nethermind.Libp2p.Protocols.IpTcp": { "type": "Project", "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "[10.0.0, )", + "Microsoft.Extensions.Logging.Abstractions": "[10.0.10, )", "Nethermind.Libp2p.Core": "[1.0.0, )" } }, @@ -649,7 +651,7 @@ "type": "Project", "dependencies": { "Makaretu.Dns.Multicast": "[0.27.0, )", - "Microsoft.Extensions.Logging.Abstractions": "[10.0.0, )", + "Microsoft.Extensions.Logging.Abstractions": "[10.0.10, )", "Nethermind.Libp2p.Core": "[1.0.0, )" } }, @@ -695,7 +697,7 @@ "type": "Project", "dependencies": { "Makaretu.Dns.Multicast": "[0.27.0, )", - "Microsoft.Extensions.Logging.Abstractions": "[10.0.0, )", + "Microsoft.Extensions.Logging.Abstractions": "[10.0.10, )", "Nethermind.Libp2p.Core": "[1.0.0, )", "Nethermind.Libp2p.Protocols.Pubsub": "[1.0.0, )" } @@ -704,7 +706,7 @@ "type": "Project", "dependencies": { "BouncyCastle.Cryptography": "[2.6.2, )", - "Microsoft.Extensions.Logging.Abstractions": "[10.0.0, )", + "Microsoft.Extensions.Logging.Abstractions": "[10.0.10, )", "Nethermind.Libp2p.Core": "[1.0.0, )" } }, @@ -712,7 +714,7 @@ "type": "Project", "dependencies": { "Google.Protobuf": "[3.33.1, )", - "Microsoft.Extensions.Logging.Abstractions": "[10.0.0, )", + "Microsoft.Extensions.Logging.Abstractions": "[10.0.10, )", "Nethermind.Libp2p.Core": "[1.0.0, )" } }, @@ -720,7 +722,7 @@ "type": "Project", "dependencies": { "Google.Protobuf": "[3.33.1, )", - "Microsoft.Extensions.Logging.Abstractions": "[10.0.0, )", + "Microsoft.Extensions.Logging.Abstractions": "[10.0.10, )", "Nethermind.Libp2p.Core": "[1.0.0, )" } }, @@ -735,12 +737,12 @@ "Nethermind.Libp2p.Protocols.WebRtc": { "type": "Project", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.0, )", - "Microsoft.Extensions.Logging.Abstractions": "[10.0.0, )", + "Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.10, )", + "Microsoft.Extensions.Logging.Abstractions": "[10.0.10, )", "Nethermind.Libp2p.Core": "[1.0.0, )", "Nethermind.Libp2p.Protocols.Noise": "[1.0.0, )", "Noise.NET": "[1.0.0, )", - "SIPSorcery": "[10.0.3, )" + "SIPSorcery": "[10.0.14, )" } }, "Nethermind.Libp2p.Protocols.WebSockets": { @@ -753,7 +755,7 @@ "Nethermind.Libp2p.Protocols.Yamux": { "type": "Project", "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "[10.0.0, )", + "Microsoft.Extensions.Logging.Abstractions": "[10.0.10, )", "Nethermind.Libp2p.Core": "[1.0.0, )" } } From 0358fc61ac5e5399de9f3730b7a4f7e942809abc Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Mon, 24 Aug 2026 10:38:24 +0300 Subject: [PATCH 02/10] Clarify WebRTC certificate construction test --- .../WebRtcDirectProtocolTests.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/libp2p/Libp2p.Protocols.WebRtc.Tests/WebRtcDirectProtocolTests.cs b/src/libp2p/Libp2p.Protocols.WebRtc.Tests/WebRtcDirectProtocolTests.cs index a237931f..866d0023 100644 --- a/src/libp2p/Libp2p.Protocols.WebRtc.Tests/WebRtcDirectProtocolTests.cs +++ b/src/libp2p/Libp2p.Protocols.WebRtc.Tests/WebRtcDirectProtocolTests.cs @@ -9,8 +9,6 @@ public class WebRtcDirectProtocolTests [Test] public void Constructor_CreatesLocalDtlsCertificate() { - WebRtcDirectProtocol protocol = new(); - - Assert.That(protocol, Is.Not.Null); + Assert.That(() => new WebRtcDirectProtocol(), Throws.Nothing); } } From 87259934b8364c75f73f3ca75a18ab0bfe81eaf7 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Mon, 24 Aug 2026 09:11:45 +0300 Subject: [PATCH 03/10] Expose TLS remote public keys --- .../TlsProtocolTests.cs | 7 +++- .../Libp2p.Protocols.Tls/TlsProtocol.cs | 40 ++++++++----------- 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/src/libp2p/Libp2p.Protocols.Tls.Tests/TlsProtocolTests.cs b/src/libp2p/Libp2p.Protocols.Tls.Tests/TlsProtocolTests.cs index 6bcfaa83..055b0421 100644 --- a/src/libp2p/Libp2p.Protocols.Tls.Tests/TlsProtocolTests.cs +++ b/src/libp2p/Libp2p.Protocols.Tls.Tests/TlsProtocolTests.cs @@ -67,7 +67,12 @@ public async Task Test_ConnectionEstablished_AfterHandshake() await downChannel.CloseAsync(); // 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))); + }); } [Test] diff --git a/src/libp2p/Libp2p.Protocols.Tls/TlsProtocol.cs b/src/libp2p/Libp2p.Protocols.Tls/TlsProtocol.cs index 9b97a5fc..b0e36b62 100644 --- a/src/libp2p/Libp2p.Protocols.Tls/TlsProtocol.cs +++ b/src/libp2p/Libp2p.Protocols.Tls/TlsProtocol.cs @@ -58,18 +58,9 @@ public async Task ListenAsync(IChannel downChannel, IConnectionContext context) await sslStream.AuthenticateAsServerAsync(serverAuthenticationOptions); LastNegotiatedApplicationProtocol = sslStream.NegotiatedApplicationProtocol; - // Extract remote peer ID from the client certificate and add it to - // RemoteAddress so UpgradeToSession (called by Yamux) can find the peer. - if (sslStream.RemoteCertificate is X509Certificate2 remoteCert - && context.State.RemoteAddress is not null - && !context.State.RemoteAddress.Has()) + if (sslStream.RemoteCertificate is X509Certificate2 remoteCert) { - Core.Dto.PublicKey? remotePubKey = CertificateHelper.ExtractPublicKey(remoteCert, out _); - if (remotePubKey != null) - { - Identity remoteIdentity = new(remotePubKey); - context.State.RemoteAddress.Add(new P2P(remoteIdentity.PeerId.ToString())); - } + SetRemoteIdentity(context, remoteCert); } _logger?.LogInformation("Server TLS Authentication successful. PeerId: {RemotePeerId}, NegotiatedProtocol: {Protocol}.", context.State.RemotePeerId, LastNegotiatedApplicationProtocol.HasValue ? System.Text.Encoding.UTF8.GetString(LastNegotiatedApplicationProtocol.Value.Protocol.ToArray()) : "None"); @@ -144,19 +135,9 @@ public async Task DialAsync(IChannel downChannel, IConnectionContext context) LastNegotiatedApplicationProtocol = sslStream.NegotiatedApplicationProtocol; - // Extract remote peer ID from the server certificate and add it to - // RemoteAddress so UpgradeToSession (called by Yamux) can find the peer. - // (TCP protocol only sets /ip4/.../tcp/... address without /p2p/... component.) - if (sslStream.RemoteCertificate is X509Certificate2 remoteCert - && context.State.RemoteAddress is not null - && !context.State.RemoteAddress.Has()) + if (sslStream.RemoteCertificate is X509Certificate2 remoteCert) { - Core.Dto.PublicKey? remotePubKey = CertificateHelper.ExtractPublicKey(remoteCert, out _); - if (remotePubKey != null) - { - Identity remoteIdentity = new(remotePubKey); - context.State.RemoteAddress.Add(new P2P(remoteIdentity.PeerId.ToString())); - } + SetRemoteIdentity(context, remoteCert); } _logger?.LogInformation("Client TLS Authentication successful. RemotePeerId: {RemotePeerId}, NegotiatedProtocol: {Protocol}.", context.State.RemotePeerId, LastNegotiatedApplicationProtocol.HasValue ? System.Text.Encoding.UTF8.GetString(LastNegotiatedApplicationProtocol.Value.Protocol.ToArray()) : "None"); @@ -182,6 +163,19 @@ public async Task DialAsync(IChannel downChannel, IConnectionContext context) } } + private static void SetRemoteIdentity(IConnectionContext context, X509Certificate2 certificate) + { + Core.Dto.PublicKey remotePublicKey = CertificateHelper.ExtractPublicKey(certificate, out _) + ?? throw new InvalidOperationException("Remote public key not found"); + + context.State.RemotePublicKey ??= remotePublicKey; + + if (context.State.RemoteAddress is { } remoteAddress && !remoteAddress.Has()) + { + remoteAddress.Add(new P2P(new Identity(remotePublicKey).PeerId.ToString())); + } + } + private static async Task ExchangeData(SslStream sslStream, IChannel upChannel, ILogger? logger) { From 2fdb69f72b12b34333db6b70d6ef26e1035c6efe Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Mon, 24 Aug 2026 10:33:45 +0300 Subject: [PATCH 04/10] Bind TLS certificate identity keys --- src/libp2p/Libp2p.Protocols.Tls.Tests/TlsProtocolTests.cs | 6 ++++-- src/libp2p/Libp2p.Protocols.Tls/TlsProtocol.cs | 8 +++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/libp2p/Libp2p.Protocols.Tls.Tests/TlsProtocolTests.cs b/src/libp2p/Libp2p.Protocols.Tls.Tests/TlsProtocolTests.cs index 055b0421..6285967f 100644 --- a/src/libp2p/Libp2p.Protocols.Tls.Tests/TlsProtocolTests.cs +++ b/src/libp2p/Libp2p.Protocols.Tls.Tests/TlsProtocolTests.cs @@ -37,14 +37,14 @@ public async Task Test_ConnectionEstablished_AfterHandshake() IConnectionContext dialerContext = Substitute.For(); 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)}" }); + dialerContext.State.Returns(new State { RemoteAddress = "/ip4/127.0.0.1/tcp/0" }); dialerContext.SubProtocols.Returns(Array.Empty()); dialerContext.Upgrade(Arg.Any()).Returns(upChannel); // Listener context (identity 2 listens for identity 1) IConnectionContext listenerContext = Substitute.For(); 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()); listenerContext.Upgrade(Arg.Any()).Returns(listenerUpChannel); @@ -72,6 +72,8 @@ public async Task Test_ConnectionEstablished_AfterHandshake() 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(listenerContext.State.RemoteAddress!.GetPeerId(), Is.EqualTo(TestPeers.PeerId(1))); }); } diff --git a/src/libp2p/Libp2p.Protocols.Tls/TlsProtocol.cs b/src/libp2p/Libp2p.Protocols.Tls/TlsProtocol.cs index b0e36b62..775e02a6 100644 --- a/src/libp2p/Libp2p.Protocols.Tls/TlsProtocol.cs +++ b/src/libp2p/Libp2p.Protocols.Tls/TlsProtocol.cs @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited // SPDX-License-Identifier: MIT +using Google.Protobuf; using System.Buffers; using System.Net.Security; using Nethermind.Libp2p.Protocols.Quic; @@ -168,7 +169,12 @@ private static void SetRemoteIdentity(IConnectionContext context, X509Certificat Core.Dto.PublicKey remotePublicKey = CertificateHelper.ExtractPublicKey(certificate, out _) ?? throw new InvalidOperationException("Remote public key not found"); - context.State.RemotePublicKey ??= remotePublicKey; + if (context.State.RemotePublicKey is { } existingRemotePublicKey && existingRemotePublicKey.ToByteString() != remotePublicKey.ToByteString()) + { + throw new InvalidOperationException("TLS certificate public key does not match the previously authenticated remote public key."); + } + + context.State.RemotePublicKey = remotePublicKey; if (context.State.RemoteAddress is { } remoteAddress && !remoteAddress.Has()) { From 6dd1f49d99e1a36a079afb791ddeecff153c4c53 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Mon, 24 Aug 2026 10:50:35 +0300 Subject: [PATCH 05/10] Cover conflicting TLS identities --- .../TlsProtocolTests.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/libp2p/Libp2p.Protocols.Tls.Tests/TlsProtocolTests.cs b/src/libp2p/Libp2p.Protocols.Tls.Tests/TlsProtocolTests.cs index 6285967f..3923a129 100644 --- a/src/libp2p/Libp2p.Protocols.Tls.Tests/TlsProtocolTests.cs +++ b/src/libp2p/Libp2p.Protocols.Tls.Tests/TlsProtocolTests.cs @@ -11,6 +11,7 @@ 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; @@ -77,6 +78,30 @@ public async Task Test_ConnectionEstablished_AfterHandshake() }); } + [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(); + 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(() => + setRemoteIdentity.Invoke(null, [context, certificate])); + + Assert.That(exception!.InnerException, Is.TypeOf()); + Assert.That(exception.InnerException!.Message, Does.Contain("does not match")); + } + [Test] public void Test_CertificateFromIdentity_CreatesValidCertificate() { From 1c6fd0ea81ee6814cdaad3ea234e87ee5de5c1ec Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Mon, 24 Aug 2026 12:40:27 +0300 Subject: [PATCH 06/10] Preserve dialed peer identity for TLS --- src/libp2p/Libp2p.Protocols.IpTcp/IpTcpProtocol.cs | 4 ++++ .../Libp2p.Protocols.Tls.Tests/TlsProtocolTests.cs | 14 ++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/src/libp2p/Libp2p.Protocols.IpTcp/IpTcpProtocol.cs b/src/libp2p/Libp2p.Protocols.IpTcp/IpTcpProtocol.cs index 7677dd17..12f69194 100644 --- a/src/libp2p/Libp2p.Protocols.IpTcp/IpTcpProtocol.cs +++ b/src/libp2p/Libp2p.Protocols.IpTcp/IpTcpProtocol.cs @@ -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() is { } requestedPeerId) + { + connectionCtx.State.RemoteAddress.Add(requestedPeerId); + } connectionCtx.State.LocalAddress = client.LocalEndPoint.ToMultiaddress(ProtocolType.Tcp); connectionCtx.Token.Register(client.Close); diff --git a/src/libp2p/Libp2p.Protocols.Tls.Tests/TlsProtocolTests.cs b/src/libp2p/Libp2p.Protocols.Tls.Tests/TlsProtocolTests.cs index 3923a129..c8287d30 100644 --- a/src/libp2p/Libp2p.Protocols.Tls.Tests/TlsProtocolTests.cs +++ b/src/libp2p/Libp2p.Protocols.Tls.Tests/TlsProtocolTests.cs @@ -102,6 +102,20 @@ public void Test_TlsIdentityConflictIsRejected() 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] public void Test_CertificateFromIdentity_CreatesValidCertificate() { From d0a84e2c7e3541e4525ea422e7c7c76d68e679af Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Mon, 24 Aug 2026 13:48:27 +0300 Subject: [PATCH 07/10] Verify Noise dialed peer identity --- .../NoiseProtocolTests.cs | 15 ++++++++ .../Libp2p.Protocols.Noise/NoiseProtocol.cs | 38 +++++++++++-------- 2 files changed, 38 insertions(+), 15 deletions(-) diff --git a/src/libp2p/Libp2p.Protocols.Noise.Tests/NoiseProtocolTests.cs b/src/libp2p/Libp2p.Protocols.Noise.Tests/NoiseProtocolTests.cs index b7bd53e7..1a07a3dd 100644 --- a/src/libp2p/Libp2p.Protocols.Noise.Tests/NoiseProtocolTests.cs +++ b/src/libp2p/Libp2p.Protocols.Noise.Tests/NoiseProtocolTests.cs @@ -3,6 +3,7 @@ using System.Buffers; using System.Buffers.Binary; +using System.Reflection; using System.Text; using Google.Protobuf; using Microsoft.Extensions.Logging; @@ -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(); + 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(() => + setRemoteIdentity.Invoke(null, [context, TestPeers.Identity(2).PublicKey])); + + Assert.That(exception!.InnerException, Is.TypeOf()); + } + [Test] public async Task Test_ConnectionEstablished_AfterHandshake() { diff --git a/src/libp2p/Libp2p.Protocols.Noise/NoiseProtocol.cs b/src/libp2p/Libp2p.Protocols.Noise/NoiseProtocol.cs index 60005050..6f4aabb8 100644 --- a/src/libp2p/Libp2p.Protocols.Noise/NoiseProtocol.cs +++ b/src/libp2p/Libp2p.Protocols.Noise/NoiseProtocol.cs @@ -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 responderMuxers = msg1Decoded.Extensions?.StreamMuxers? .Where(m => !string.IsNullOrEmpty(m)) .ToList() ?? []; @@ -115,11 +112,7 @@ public async Task DialAsync(IChannel downChannel, IConnectionContext context) }; } - PeerId remotePeerId = new(msg1KeyDecoded); - if (!context.State.RemoteAddress.Has()) - { - 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); @@ -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 initiatorMuxers = msg2Decoded.Extensions?.StreamMuxers?.Where(m => !string.IsNullOrEmpty(m)).ToList() ?? []; @@ -245,11 +236,7 @@ public async Task ListenAsync(IChannel downChannel, IConnectionContext context) }; } - if (!context.State.RemoteAddress.Has()) - { - PeerId remotePeerId = new(msg2KeyDecoded); - context.State.RemoteAddress.Add(new P2P(remotePeerId.ToString())); - } + SetRemoteIdentity(context, msg2KeyDecoded); _logger?.LogDebug("Established connection to {peer}", context.State.RemoteAddress); @@ -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 From 80940fa2d5954ace43bf0454d2da5f61b7cf853e Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Tue, 22 Sep 2026 18:41:29 +0300 Subject: [PATCH 08/10] Address independent review feedback --- .github/workflows/test.yml | 2 + .../IpTcpPeerIdentityTests.cs | 55 +++++++++++++++++++ .../Libp2p.Protocols.Tls.Tests.csproj | 1 + .../TlsProtocolTests.cs | 55 ++++++++++++++----- .../Libp2p.Protocols.Tls/TlsProtocol.cs | 5 +- 5 files changed, 103 insertions(+), 15 deletions(-) create mode 100644 src/libp2p/Libp2p.Protocols.Tls.Tests/IpTcpPeerIdentityTests.cs diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 14ccbd4c..01c54608 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -55,6 +55,8 @@ jobs: 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 }} + # Invoke the executable NUnit runner; the nested global.json does not select MTP for dotnet test. + dotnet run --project 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 }} diff --git a/src/libp2p/Libp2p.Protocols.Tls.Tests/IpTcpPeerIdentityTests.cs b/src/libp2p/Libp2p.Protocols.Tls.Tests/IpTcpPeerIdentityTests.cs new file mode 100644 index 00000000..e7ee90eb --- /dev/null +++ b/src/libp2p/Libp2p.Protocols.Tls.Tests/IpTcpPeerIdentityTests.cs @@ -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 upgradedAddress = new(TaskCreationOptions.RunContinuationsAsynchronously); + INewConnectionContext connection = Substitute.For(); + connection.State.Returns(state); + connection.Upgrade(Arg.Any()).Returns(_ => + { + // Capture the address before a security protocol can enrich it. + upgradedAddress.SetResult(state.RemoteAddress!.ToString()); + return channel; + }); + ITransportContext context = Substitute.For(); + 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)); + } + } +} diff --git a/src/libp2p/Libp2p.Protocols.Tls.Tests/Libp2p.Protocols.Tls.Tests.csproj b/src/libp2p/Libp2p.Protocols.Tls.Tests/Libp2p.Protocols.Tls.Tests.csproj index 71a0a560..5573c036 100644 --- a/src/libp2p/Libp2p.Protocols.Tls.Tests/Libp2p.Protocols.Tls.Tests.csproj +++ b/src/libp2p/Libp2p.Protocols.Tls.Tests/Libp2p.Protocols.Tls.Tests.csproj @@ -26,6 +26,7 @@ + diff --git a/src/libp2p/Libp2p.Protocols.Tls.Tests/TlsProtocolTests.cs b/src/libp2p/Libp2p.Protocols.Tls.Tests/TlsProtocolTests.cs index c8287d30..64fbc6c1 100644 --- a/src/libp2p/Libp2p.Protocols.Tls.Tests/TlsProtocolTests.cs +++ b/src/libp2p/Libp2p.Protocols.Tls.Tests/TlsProtocolTests.cs @@ -4,6 +4,7 @@ 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; @@ -23,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(); @@ -33,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(); 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 = "/ip4/127.0.0.1/tcp/0" }); + 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()); - dialerContext.Upgrade(Arg.Any()).Returns(upChannel); + dialerContext.Upgrade(Arg.Any()).Returns(_ => + { + dialerUpgraded.SetResult(); + return upChannel; + }); // Listener context (identity 2 listens for identity 1) IConnectionContext listenerContext = Substitute.For(); listenerContext.Peer.Identity.Returns(TestPeers.Identity(2)); listenerContext.State.Returns(new State { RemoteAddress = "/ip4/127.0.0.1/tcp/0" }); listenerContext.SubProtocols.Returns(Array.Empty()); - listenerContext.Upgrade(Arg.Any()).Returns(listenerUpChannel); + listenerContext.Upgrade(Arg.Any()).Returns(_ => + { + listenerUpgraded.SetResult(); + return listenerUpChannel; + }); MultiplexerSettings i_multiplexerSettings = new(); MultiplexerSettings r_multiplexerSettings = new(); @@ -59,13 +74,26 @@ public async Task Test_ConnectionEstablished_AfterHandshake() Task dialTask = tlsProtocolInitiator.DialAsync(downChannelFromProtocolPov, dialerContext); int sent = 42; - ValueTask 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 writeTask = listenerUpChannel.Reverse().WriteVarintAsync(sent); + received = await upChannel.Reverse().ReadVarintAsync().WaitAsync(TimeSpan.FromSeconds(15)); + await writeTask; + } + finally + { + await upChannel.CloseAsync(); + await listenerUpChannel.CloseAsync(); + await downChannel.CloseAsync(); + await Task.WhenAll(listenTask, dialTask).WaitAsync(TimeSpan.FromSeconds(15)); + } // Assert Assert.Multiple(() => @@ -74,6 +102,7 @@ public async Task Test_ConnectionEstablished_AfterHandshake() 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))); }); } @@ -98,7 +127,7 @@ public void Test_TlsIdentityConflictIsRejected() TargetInvocationException? exception = Assert.Throws(() => setRemoteIdentity.Invoke(null, [context, certificate])); - Assert.That(exception!.InnerException, Is.TypeOf()); + Assert.That(exception!.InnerException, Is.TypeOf()); Assert.That(exception.InnerException!.Message, Does.Contain("does not match")); } diff --git a/src/libp2p/Libp2p.Protocols.Tls/TlsProtocol.cs b/src/libp2p/Libp2p.Protocols.Tls/TlsProtocol.cs index 775e02a6..a3024f7a 100644 --- a/src/libp2p/Libp2p.Protocols.Tls/TlsProtocol.cs +++ b/src/libp2p/Libp2p.Protocols.Tls/TlsProtocol.cs @@ -9,6 +9,7 @@ using Microsoft.Extensions.Logging; using System.Security.Cryptography; using Nethermind.Libp2p.Core; +using Nethermind.Libp2p.Core.Exceptions; using Multiformats.Address; using Multiformats.Address.Protocols; using System.Text; @@ -167,11 +168,11 @@ public async Task DialAsync(IChannel downChannel, IConnectionContext context) private static void SetRemoteIdentity(IConnectionContext context, X509Certificate2 certificate) { Core.Dto.PublicKey remotePublicKey = CertificateHelper.ExtractPublicKey(certificate, out _) - ?? throw new InvalidOperationException("Remote public key not found"); + ?? throw new Libp2pException("Remote public key not found"); if (context.State.RemotePublicKey is { } existingRemotePublicKey && existingRemotePublicKey.ToByteString() != remotePublicKey.ToByteString()) { - throw new InvalidOperationException("TLS certificate public key does not match the previously authenticated remote public key."); + throw new Libp2pException("TLS certificate public key does not match the previously authenticated remote public key."); } context.State.RemotePublicKey = remotePublicKey; From 250ca5a63f6f56c360a984ecf868c3b2edf38d06 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Wed, 23 Sep 2026 12:39:18 +0300 Subject: [PATCH 09/10] Run all CI tests and bound TLS handshake cleanup --- .github/workflows/test.yml | 5 ++--- src/libp2p/Libp2p.Protocols.Tls.Tests/TlsProtocolTests.cs | 7 ++++--- src/libp2p/global.json | 3 +++ 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 01c54608..aebda45c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -48,15 +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 }} - # Invoke the executable NUnit runner; the nested global.json does not select MTP for dotnet test. - dotnet run --project Libp2p.Protocols.Tls.Tests/Libp2p.Protocols.Tls.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 }} diff --git a/src/libp2p/Libp2p.Protocols.Tls.Tests/TlsProtocolTests.cs b/src/libp2p/Libp2p.Protocols.Tls.Tests/TlsProtocolTests.cs index 64fbc6c1..56e3ba9d 100644 --- a/src/libp2p/Libp2p.Protocols.Tls.Tests/TlsProtocolTests.cs +++ b/src/libp2p/Libp2p.Protocols.Tls.Tests/TlsProtocolTests.cs @@ -89,9 +89,10 @@ public async Task Test_ConnectionEstablished_AfterHandshake(bool includePeerId) } finally { - await upChannel.CloseAsync(); - await listenerUpChannel.CloseAsync(); - await downChannel.CloseAsync(); + 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)); } diff --git a/src/libp2p/global.json b/src/libp2p/global.json index 4d06b19d..bd733c99 100644 --- a/src/libp2p/global.json +++ b/src/libp2p/global.json @@ -3,5 +3,8 @@ "version": "9.0.304", "rollForward": "latestMajor", "allowPrerelease": true + }, + "test": { + "runner": "Microsoft.Testing.Platform" } } From 76c8826ff8f822e7ee3161a8f5445c9fa23cfed4 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Wed, 23 Sep 2026 13:00:30 +0300 Subject: [PATCH 10/10] Preserve unsigned secp256k1 private key values --- src/libp2p/Libp2p.Core.Tests/IdentityTests.cs | 23 +++++++++++++++++++ src/libp2p/Libp2p.Core/Identity.cs | 6 +++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/libp2p/Libp2p.Core.Tests/IdentityTests.cs b/src/libp2p/Libp2p.Core.Tests/IdentityTests.cs index 5c6eee5d..188558f8 100644 --- a/src/libp2p/Libp2p.Core.Tests/IdentityTests.cs +++ b/src/libp2p/Libp2p.Core.Tests/IdentityTests.cs @@ -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); + } } diff --git a/src/libp2p/Libp2p.Core/Identity.cs b/src/libp2p/Libp2p.Core/Identity.cs index 9a903d80..981abfe8 100644 --- a/src/libp2p/Libp2p.Core/Identity.cs +++ b/src/libp2p/Libp2p.Core/Identity.cs @@ -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 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)); } @@ -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;