Skip to content

Commit

Permalink
Added TLS Protocol (#104)
Browse files Browse the repository at this point in the history
Co-authored-by: Alexey <[email protected]>
  • Loading branch information
rose2221 and flcl42 authored Sep 24, 2024
1 parent cc62778 commit 3464c85
Show file tree
Hide file tree
Showing 8 changed files with 336 additions and 12 deletions.
105 changes: 105 additions & 0 deletions src/libp2p/Libp2p.Core/Stream.cs
Original file line number Diff line number Diff line change
@@ -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<ChannelStream> logger;
private bool _disposed = false;
private bool _canRead = true;
private bool _canWrite = true;

// Constructor
public ChannelStream(IChannel chan)

Check warning on line 17 in src/libp2p/Libp2p.Core/Stream.cs

View workflow job for this annotation

GitHub Actions / Test

Non-nullable field 'logger' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable.

Check warning on line 17 in src/libp2p/Libp2p.Core/Stream.cs

View workflow job for this annotation

GitHub Actions / Test

Non-nullable field 'logger' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable.
{
_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<byte> 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<byte>(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<byte>(buffer.AsMemory(offset, count)))) != IOResult.Ok)
{
_canWrite = false;
}
}

public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
=> base.WriteAsync(buffer, cancellationToken);

public override async Task<int> 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<int> ReadAsync(Memory<byte> 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);
}
}
29 changes: 29 additions & 0 deletions src/libp2p/Libp2p.Protocols.Tls/Libp2p.Protocols.Tls.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<AssemblyName>Nethermind.$(MSBuildProjectName)</AssemblyName>
<RootNamespace>Nethermind.$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
</PropertyGroup>

<PropertyGroup>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageTags>libp2p network tls</PackageTags>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Libp2p.Core\Libp2p.Core.csproj" />
<ProjectReference Include="..\Libp2p.Protocols.Quic\Libp2p.Protocols.Quic.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging" />
</ItemGroup>

<ItemGroup>
<None Include="README.md" Pack="true" PackagePath="/" />
</ItemGroup>

</Project>
4 changes: 4 additions & 0 deletions src/libp2p/Libp2p.Protocols.Tls/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Tls protocol

- [libp2p spec](https://github.com/libp2p/specs/blob/master/tls/tls.md)

183 changes: 183 additions & 0 deletions src/libp2p/Libp2p.Protocols.Tls/TlsProtocol.cs
Original file line number Diff line number Diff line change
@@ -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<TlsProtocol>? _logger = loggerFactory?.CreateLogger<TlsProtocol>();
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<P2P>());
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<P2P>(), certificate.Subject, certificate.Issuer);

var _protocols = multiplexerSettings is null ?
new List<SslApplicationProtocol> { } :
!multiplexerSettings.Multiplexers.Any() ?
new List<SslApplicationProtocol> { } :
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<P2P>(), sslStream.NegotiatedApplicationProtocol.Protocol);
}
catch (Exception ex)
{
_logger?.LogError("Error during TLS authentication for PeerId {RemotePeerId}: {ErrorMessage}.", context.RemotePeer.Address.Get<P2P>(), 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<P2P>().ToString());

public async Task DialAsync(IChannel downChannel, IChannelFactory? channelFactory, IPeerContext context)
{
_logger?.LogInformation("Starting DialAsync: LocalPeerId {LocalPeerId}", context.LocalPeer.Address.Get<P2P>());
if (channelFactory is null)
{
throw new ArgumentException("Protocol is not properly instantiated");
}
Multiaddress addr = context.LocalPeer.Address;
bool isIP4 = addr.Has<IP4>();
MultiaddressProtocol ipProtocol = isIP4 ? addr.Get<IP4>() : addr.Get<IP6>();
IPAddress ipAddress = IPAddress.Parse(ipProtocol.ToString());

var _protocols = multiplexerSettings is null ?
new List<SslApplicationProtocol> { } :
!multiplexerSettings.Multiplexers.Any() ?
new List<SslApplicationProtocol> { } :
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<P2P>());
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<P2P>(), sslStream.NegotiatedApplicationProtocol.Protocol);
}
catch (Exception ex)
{
_logger?.LogError("Error during TLS client authentication for RemotePeerId {RemotePeerId}: {ErrorMessage}.", context.RemotePeer.Address.Get<P2P>(), 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<P2P>());
await ExchangeData(sslStream, upChannel, _logger);
_logger?.LogDebug("Connection closed for PeerId {RemotePeerId}.", context.RemotePeer.Address.Get<P2P>());
_ = upChannel.CloseAsync();
}

private static async Task ExchangeData(SslStream sslStream, IChannel upChannel, ILogger<TlsProtocol>? 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<byte> 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<byte>(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);
}
}

6 changes: 6 additions & 0 deletions src/libp2p/Libp2p.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/libp2p/Libp2p/Libp2p.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<ProjectReference Include="..\Libp2p.Protocols.Ping\Libp2p.Protocols.Ping.csproj" />
<ProjectReference Include="..\Libp2p.Protocols.Plaintext\Libp2p.Protocols.Plaintext.csproj" />
<ProjectReference Include="..\Libp2p.Protocols.Yamux\Libp2p.Protocols.Yamux.csproj" />
<ProjectReference Include="..\Libp2p.Protocols.Tls\Libp2p.Protocols.Tls.csproj" />
</ItemGroup>

<ItemGroup>
Expand Down
Loading

0 comments on commit 3464c85

Please sign in to comment.