diff --git a/src/libp2p/Libp2p.Core/Stream.cs b/src/libp2p/Libp2p.Core/Stream.cs new file mode 100644 index 00000000..4b1d1720 --- /dev/null +++ b/src/libp2p/Libp2p.Core/Stream.cs @@ -0,0 +1,105 @@ +// SPDX-FileCopyrightText: 2023 Demerzel Solutions Limited +// SPDX-License-Identifier: MIT + +using Microsoft.Extensions.Logging; +using Nethermind.Libp2p.Core; +using System.Buffers; + +public class ChannelStream : Stream +{ + private readonly IChannel _chan; + private readonly ILogger logger; + private bool _disposed = false; + private bool _canRead = true; + private bool _canWrite = true; + + // Constructor + public ChannelStream(IChannel chan) + { + _chan = chan ?? throw new ArgumentNullException(nameof(_chan)); + } + + public override bool CanRead => _canRead; + public override bool CanSeek => false; + public override bool CanWrite => _canWrite; + public override long Length => throw new Exception(); + + public override long Position + { + get => 0; + set => throw new NotSupportedException(); + } + + public override void Flush() { } + + public override int Read(byte[] buffer, int offset, int count) => Read(buffer.AsSpan(offset, count)); + + public override int Read(Span buffer) + { + if (buffer is { Length: 0 } && _canRead) return 0; + + var result = _chan.ReadAsync(buffer.Length, ReadBlockingMode.WaitAny).Result; + if (result.Result != IOResult.Ok) + { + _canRead = false; + return 0; + } + + result.Data.CopyTo(buffer); + return (int)result.Data.Length; + } + + public override void Write(byte[] buffer, int offset, int count) + { + if (_chan.WriteAsync(new ReadOnlySequence(buffer.AsMemory(offset, count))).Result != IOResult.Ok) + { + _canWrite = false; + } + } + + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if ((await _chan.WriteAsync(new ReadOnlySequence(buffer.AsMemory(offset, count)))) != IOResult.Ok) + { + _canWrite = false; + } + } + + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + => base.WriteAsync(buffer, cancellationToken); + + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (buffer is { Length: 0 } && _canRead) return 0; + + var result = await _chan.ReadAsync(buffer.Length, ReadBlockingMode.WaitAny); + if (result.Result != IOResult.Ok) + { + _canRead = false; + return 0; + } + + result.Data.CopyTo(buffer); + return (int)result.Data.Length; + } + + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + => base.ReadAsync(buffer, cancellationToken); + + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + + public override void SetLength(long value) => throw new NotSupportedException(); + + protected override void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + _ = _chan.CloseAsync(); + } + _disposed = true; + } + base.Dispose(disposing); + } +} diff --git a/src/libp2p/Libp2p.Protocols.Tls/Libp2p.Protocols.Tls.csproj b/src/libp2p/Libp2p.Protocols.Tls/Libp2p.Protocols.Tls.csproj new file mode 100644 index 00000000..1380be09 --- /dev/null +++ b/src/libp2p/Libp2p.Protocols.Tls/Libp2p.Protocols.Tls.csproj @@ -0,0 +1,29 @@ + + + + enable + enable + latest + Nethermind.$(MSBuildProjectName) + Nethermind.$(MSBuildProjectName.Replace(" ", "_")) + + + + README.md + libp2p network tls + + + + + + + + + + + + + + + + diff --git a/src/libp2p/Libp2p.Protocols.Tls/README.md b/src/libp2p/Libp2p.Protocols.Tls/README.md new file mode 100644 index 00000000..045788ad --- /dev/null +++ b/src/libp2p/Libp2p.Protocols.Tls/README.md @@ -0,0 +1,4 @@ +# Tls protocol + +- [libp2p spec](https://github.com/libp2p/specs/blob/master/tls/tls.md) + diff --git a/src/libp2p/Libp2p.Protocols.Tls/TlsProtocol.cs b/src/libp2p/Libp2p.Protocols.Tls/TlsProtocol.cs new file mode 100644 index 00000000..839d513f --- /dev/null +++ b/src/libp2p/Libp2p.Protocols.Tls/TlsProtocol.cs @@ -0,0 +1,183 @@ +using System.Buffers; +using System.Net; +using System.Net.Security; +using Nethermind.Libp2p.Protocols.Quic; +using System.Security.Cryptography.X509Certificates; +using Microsoft.Extensions.Logging; +using System.Security.Cryptography; +using Nethermind.Libp2p.Core; +using Multiformats.Address; +using Multiformats.Address.Protocols; +using System.Text; + +namespace Nethermind.Libp2p.Protocols; + +public class TlsProtocol(MultiplexerSettings? multiplexerSettings = null, ILoggerFactory? loggerFactory = null) : IProtocol +{ + private readonly ECDsa _sessionKey = ECDsa.Create(); + private readonly ILogger? _logger = loggerFactory?.CreateLogger(); + public SslApplicationProtocol? LastNegotiatedApplicationProtocol { get; private set; } + public string Id => "/tls/1.0.0"; + + public async Task ListenAsync(IChannel downChannel, IChannelFactory? channelFactory, IPeerContext context) + { + _logger?.LogInformation("Starting ListenAsync: PeerId {LocalPeerId}", context.LocalPeer.Address.Get()); + if (channelFactory is null) + { + throw new ArgumentException("Protocol is not properly instantiated"); + } + Stream str = new ChannelStream(downChannel); + X509Certificate certificate = CertificateHelper.CertificateFromIdentity(_sessionKey, context.LocalPeer.Identity); + _logger?.LogDebug("Successfully created X509Certificate for PeerId {LocalPeerId}. Certificate Subject: {Subject}, Issuer: {Issuer}", context.LocalPeer.Address.Get(), certificate.Subject, certificate.Issuer); + + var _protocols = multiplexerSettings is null ? + new List { } : + !multiplexerSettings.Multiplexers.Any() ? + new List { } : + multiplexerSettings.Multiplexers.Select(proto => new SslApplicationProtocol(proto.Id)).ToList(); + + SslServerAuthenticationOptions serverAuthenticationOptions = new() + { + ApplicationProtocols = _protocols, + RemoteCertificateValidationCallback = (_, certificate, _, _) => VerifyRemoteCertificate(context.RemotePeer.Address, certificate), + ServerCertificate = certificate, + ClientCertificateRequired = true, + }; + _logger?.LogTrace("SslServerAuthenticationOptions initialized with ApplicationProtocols: {Protocols}.", string.Join(", ", _protocols.Select(p => p.Protocol))); + SslStream sslStream = new(str, false, serverAuthenticationOptions.RemoteCertificateValidationCallback); + _logger?.LogTrace("SslStream initialized."); + try + { + await sslStream.AuthenticateAsServerAsync(serverAuthenticationOptions); + _logger?.LogInformation("Server TLS Authentication successful. PeerId: {RemotePeerId}, NegotiatedProtocol: {Protocol}.", context.RemotePeer.Address.Get(), sslStream.NegotiatedApplicationProtocol.Protocol); + } + catch (Exception ex) + { + _logger?.LogError("Error during TLS authentication for PeerId {RemotePeerId}: {ErrorMessage}.", context.RemotePeer.Address.Get(), ex.Message); + _logger?.LogDebug("TLS Authentication Exception Details: {StackTrace}", ex.StackTrace); + throw; + } + _logger?.LogDebug($"{Encoding.UTF8.GetString(sslStream.NegotiatedApplicationProtocol.Protocol.ToArray())} protocol negotiated"); + IChannel upChannel = channelFactory.SubListen(context); + await ExchangeData(sslStream, upChannel, _logger); + _ = upChannel.CloseAsync(); + } + + private static bool VerifyRemoteCertificate(Multiaddress remotePeerAddress, X509Certificate certificate) => + CertificateHelper.ValidateCertificate(certificate as X509Certificate2, remotePeerAddress.Get().ToString()); + + public async Task DialAsync(IChannel downChannel, IChannelFactory? channelFactory, IPeerContext context) + { + _logger?.LogInformation("Starting DialAsync: LocalPeerId {LocalPeerId}", context.LocalPeer.Address.Get()); + if (channelFactory is null) + { + throw new ArgumentException("Protocol is not properly instantiated"); + } + Multiaddress addr = context.LocalPeer.Address; + bool isIP4 = addr.Has(); + MultiaddressProtocol ipProtocol = isIP4 ? addr.Get() : addr.Get(); + IPAddress ipAddress = IPAddress.Parse(ipProtocol.ToString()); + + var _protocols = multiplexerSettings is null ? + new List { } : + !multiplexerSettings.Multiplexers.Any() ? + new List { } : + multiplexerSettings.Multiplexers.Select(proto => new SslApplicationProtocol(proto.Id)).ToList(); + + SslClientAuthenticationOptions clientAuthenticationOptions = new() + { + CertificateChainPolicy = new X509ChainPolicy + { + RevocationMode = X509RevocationMode.NoCheck, + VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority + }, + TargetHost = ipAddress.ToString(), + ApplicationProtocols = _protocols, + EnabledSslProtocols = System.Security.Authentication.SslProtocols.Tls13, + RemoteCertificateValidationCallback = (_, certificate, _, _) => VerifyRemoteCertificate(context.RemotePeer.Address, certificate), + ClientCertificates = new X509CertificateCollection { CertificateHelper.CertificateFromIdentity(_sessionKey, context.LocalPeer.Identity) }, + }; + _logger?.LogTrace("SslClientAuthenticationOptions initialized for PeerId {RemotePeerId}.", context.RemotePeer.Address.Get()); + Stream str = new ChannelStream(downChannel); + SslStream sslStream = new(str, false, clientAuthenticationOptions.RemoteCertificateValidationCallback); + _logger?.LogTrace("Sslstream initialized."); + try + { + await sslStream.AuthenticateAsClientAsync(clientAuthenticationOptions); + _logger?.LogInformation("Client TLS Authentication successful. RemotePeerId: {RemotePeerId}, NegotiatedProtocol: {Protocol}.", context.RemotePeer.Address.Get(), sslStream.NegotiatedApplicationProtocol.Protocol); + } + catch (Exception ex) + { + _logger?.LogError("Error during TLS client authentication for RemotePeerId {RemotePeerId}: {ErrorMessage}.", context.RemotePeer.Address.Get(), ex.Message); + _logger?.LogDebug("TLS Authentication Exception Details: {StackTrace}", ex.StackTrace); + return; + } + _logger?.LogDebug("Subdialing protocols: {Protocols}.", string.Join(", ", channelFactory.SubProtocols.Select(x => x.Id))); + IChannel upChannel = channelFactory.SubDial(context); + _logger?.LogDebug("SubDial completed for PeerId {RemotePeerId}.", context.RemotePeer.Address.Get()); + await ExchangeData(sslStream, upChannel, _logger); + _logger?.LogDebug("Connection closed for PeerId {RemotePeerId}.", context.RemotePeer.Address.Get()); + _ = upChannel.CloseAsync(); + } + + private static async Task ExchangeData(SslStream sslStream, IChannel upChannel, ILogger? logger) + { + upChannel.GetAwaiter().OnCompleted(() => + { + sslStream.Close(); + logger?.LogDebug("Stream: Closed"); + }); + logger?.LogTrace("Starting data exchange between sslStream and upChannel."); + Task writeTask = Task.Run(async () => + { + try + { + logger?.LogDebug("Starting to write to sslStream"); + await foreach (ReadOnlySequence data in upChannel.ReadAllAsync()) + { + logger.LogDebug($"Got data to send to peer: {{{Encoding.UTF8.GetString(data).Replace("\n", "\\n").Replace("\r", "\\r")}}}!"); + await sslStream.WriteAsync(data.ToArray()); + await sslStream.FlushAsync(); + logger.LogDebug($"Data sent to sslStream {{{Encoding.UTF8.GetString(data).Replace("\n", "\\n").Replace("\r", "\\r")}}}!!"); + } + } + catch (Exception ex) + { + logger?.LogError(ex, "Error while writing to sslStream"); + await upChannel.CloseAsync(); + } + }); + Task readTask = Task.Run(async () => + { + try + { + logger?.LogDebug("Starting to read from sslStream"); + while (true) + { + byte[] data = new byte[1024]; + int len = await sslStream.ReadAtLeastAsync(data, 1, false); + if (len != 0) + { + logger?.LogDebug($"Received {len} bytes from sslStream: {{{Encoding.UTF8.GetString(data, 0, len).Replace("\r", "\\r").Replace("\n", "\\n")}}}"); + try + { + await upChannel.WriteAsync(new ReadOnlySequence(data.ToArray()[..len])); + } + catch (Exception ex) + { + logger?.LogError(ex, "Error while reading from sslStream"); + } + logger.LogDebug($"Data received from sslStream, {len}"); + } + } + await upChannel.WriteEofAsync(); + } + catch (Exception ex) + { + logger?.LogError(ex, "Error while reading from sslStream"); + } + }); + await Task.WhenAll(writeTask, readTask); + } +} + diff --git a/src/libp2p/Libp2p.sln b/src/libp2p/Libp2p.sln index b818ff76..241485d4 100644 --- a/src/libp2p/Libp2p.sln +++ b/src/libp2p/Libp2p.sln @@ -65,6 +65,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Libp2p.Protocols.Yamux.Test EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TransportInterop", "..\samples\transport-interop\TransportInterop.csproj", "{EC505F21-FC69-4432-88A8-3CD5F7899B08}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Libp2p.Protocols.Tls", "Libp2p.Protocols.Tls\Libp2p.Protocols.Tls.csproj", "{C3CDBAAE-C790-443A-A293-D6E2330160F7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -175,6 +177,10 @@ Global {EC505F21-FC69-4432-88A8-3CD5F7899B08}.Debug|Any CPU.Build.0 = Debug|Any CPU {EC505F21-FC69-4432-88A8-3CD5F7899B08}.Release|Any CPU.ActiveCfg = Release|Any CPU {EC505F21-FC69-4432-88A8-3CD5F7899B08}.Release|Any CPU.Build.0 = Release|Any CPU + {C3CDBAAE-C790-443A-A293-D6E2330160F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C3CDBAAE-C790-443A-A293-D6E2330160F7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C3CDBAAE-C790-443A-A293-D6E2330160F7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C3CDBAAE-C790-443A-A293-D6E2330160F7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/libp2p/Libp2p/Libp2p.csproj b/src/libp2p/Libp2p/Libp2p.csproj index da269e33..b15288a3 100644 --- a/src/libp2p/Libp2p/Libp2p.csproj +++ b/src/libp2p/Libp2p/Libp2p.csproj @@ -22,6 +22,7 @@ + diff --git a/src/libp2p/Libp2p/Libp2pPeerFactoryBuilder.cs b/src/libp2p/Libp2p/Libp2pPeerFactoryBuilder.cs index 1d720ccd..05df21d3 100644 --- a/src/libp2p/Libp2p/Libp2pPeerFactoryBuilder.cs +++ b/src/libp2p/Libp2p/Libp2pPeerFactoryBuilder.cs @@ -1,15 +1,16 @@ // SPDX-FileCopyrightText: 2023 Demerzel Solutions Limited // SPDX-License-Identifier: MIT +using Microsoft.Extensions.Logging; using Nethermind.Libp2p.Core; using Nethermind.Libp2p.Protocols; using Nethermind.Libp2p.Protocols.Pubsub; +using System.Net.Security; using System.Runtime.Versioning; namespace Nethermind.Libp2p.Stack; -public class Libp2pPeerFactoryBuilder : PeerFactoryBuilderBase, - ILibp2pPeerFactoryBuilder +public class Libp2pPeerFactoryBuilder : PeerFactoryBuilderBase, ILibp2pPeerFactoryBuilder { private bool enforcePlaintext; @@ -19,25 +20,19 @@ public ILibp2pPeerFactoryBuilder WithPlaintextEnforced() return this; } - public Libp2pPeerFactoryBuilder(IServiceProvider? serviceProvider = default) : base(serviceProvider) - { - } + public Libp2pPeerFactoryBuilder(IServiceProvider? serviceProvider = default) : base(serviceProvider) { } protected override ProtocolStack BuildStack() { - ProtocolStack tcpEncryptionStack = enforcePlaintext ? - Over() : - Over(); + ProtocolStack tcpEncryptionStack = enforcePlaintext ? Over() : Over().Or(); - ProtocolStack tcpStack = - Over() + ProtocolStack tcpStack = Over() .Over() .Over(tcpEncryptionStack) .Over() .Over(); - return - Over() + return Over() // Quic is not working well, and requires consumers to mark projects with preview //.Over().Or(tcpStack) .Over(tcpStack) diff --git a/src/samples/chat/Chat.csproj b/src/samples/chat/Chat.csproj index 39276f75..b2bbc7a7 100644 --- a/src/samples/chat/Chat.csproj +++ b/src/samples/chat/Chat.csproj @@ -20,6 +20,7 @@ +