Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,6 @@ internal static partial class Interop
{
internal static partial class OpenSsl
{
#if DEBUG
private static readonly string? s_keyLogFile = Environment.GetEnvironmentVariable("SSLKEYLOGFILE");
private static readonly FileStream? s_fileStream = s_keyLogFile != null ? File.Open(s_keyLogFile, FileMode.Append, FileAccess.Write, FileShare.ReadWrite) : null;
#endif
private const string TlsCacheSizeCtxName = "System.Net.Security.TlsCacheSize";
private const string TlsCacheSizeEnvironmentVariable = "DOTNET_SYSTEM_NET_SECURITY_TLSCACHESIZE";
private const SslProtocols FakeAlpnSslProtocol = (SslProtocols)1; // used to distinguish server sessions with ALPN
Expand Down Expand Up @@ -209,12 +205,10 @@ internal static unsafe SafeSslContextHandle AllocateSslContext(SslAuthentication
Ssl.SslCtxSetDefaultOcspCallback(sslCtx);
}
}
#if DEBUG
if (s_fileStream != null)
if (SslKeyLogger.IsEnabled)
{
Ssl.SslCtxSetKeylogCallback(sslCtx, &KeyLogCallback);
}
#endif
}
catch
{
Expand Down Expand Up @@ -757,23 +751,12 @@ private static unsafe void RemoveSessionCallback(IntPtr ctx, IntPtr session)
ctxHandle.RemoveSession(name);
}

#if DEBUG
[UnmanagedCallersOnly]
private static unsafe void KeyLogCallback(IntPtr ssl, char* line)
{
Debug.Assert(s_fileStream != null);
ReadOnlySpan<byte> data = MemoryMarshal.CreateReadOnlySpanFromNullTerminated((byte*)line);
if (data.Length > 0)
{
lock (s_fileStream)
{
s_fileStream.Write(data);
s_fileStream.WriteByte((byte)'\n');
s_fileStream.Flush();
}
}
SslKeyLogger.WriteLineRaw(data);
}
#endif

private static int BioRead(SafeBioHandle bio, Span<byte> buffer, int count)
{
Expand Down
134 changes: 134 additions & 0 deletions src/libraries/Common/src/System/Net/Security/SslKeyLogger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Diagnostics;
using System.IO;
using System.Net;

internal static class SslKeyLogger
{
private static readonly string? s_keyLogFile = Environment.GetEnvironmentVariable("SSLKEYLOGFILE");
private static readonly FileStream? s_fileStream;

#pragma warning disable CA1810 // Initialize all static fields when declared and remove cctor
static SslKeyLogger()
{
s_fileStream = null;

try
{
#if DEBUG
bool isEnabled = true;
#else
bool isEnabled = AppContext.TryGetSwitch("System.Net.EnableSslKeyLogging", out bool enabled) && enabled;
Comment thread
rzikm marked this conversation as resolved.
#endif

if (isEnabled && s_keyLogFile != null)
{
s_fileStream = File.Open(s_keyLogFile, FileMode.Append, FileAccess.Write, FileShare.ReadWrite);
}
}
catch (Exception ex)
{
if (NetEventSource.Log.IsEnabled())
{
NetEventSource.Error(null, $"Failed to open SSL key log file '{s_keyLogFile}': {ex}");
}
}
}
#pragma warning restore CA1810

public static bool IsEnabled => s_fileStream != null;

public static void WriteLineRaw(ReadOnlySpan<byte> data)
{
Debug.Assert(s_fileStream != null);
if (s_fileStream == null)
{
return;
}

if (data.Length > 0)
{
lock (s_fileStream)
{
s_fileStream.Write(data);
s_fileStream.WriteByte((byte)'\n');
s_fileStream.Flush();
}
}
}

public static void WriteSecrets(
ReadOnlySpan<byte> clientRandom,
ReadOnlySpan<byte> clientHandshakeTrafficSecret,
ReadOnlySpan<byte> serverHandshakeTrafficSecret,
ReadOnlySpan<byte> clientTrafficSecret0,
ReadOnlySpan<byte> serverTrafficSecret0,
ReadOnlySpan<byte> clientEarlyTrafficSecret)
{
Debug.Assert(s_fileStream != null);
Debug.Assert(!clientRandom.IsEmpty);

if (s_fileStream == null ||
clientRandom.IsEmpty ||

// return early if there is nothing to log
(clientHandshakeTrafficSecret.IsEmpty &&
serverHandshakeTrafficSecret.IsEmpty &&
clientTrafficSecret0.IsEmpty &&
serverTrafficSecret0.IsEmpty &&
clientEarlyTrafficSecret.IsEmpty))
{
return;
}

Span<byte> clientRandomUtf8 = clientRandom.Length <= 1024 ? stackalloc byte[clientRandom.Length * 2] : new byte[clientRandom.Length * 2];
HexEncode(clientRandom, clientRandomUtf8);

lock (s_fileStream)
{
WriteSecretCore("CLIENT_HANDSHAKE_TRAFFIC_SECRET"u8, clientRandomUtf8, clientHandshakeTrafficSecret);
WriteSecretCore("SERVER_HANDSHAKE_TRAFFIC_SECRET"u8, clientRandomUtf8, serverHandshakeTrafficSecret);
WriteSecretCore("CLIENT_TRAFFIC_SECRET_0"u8, clientRandomUtf8, clientTrafficSecret0);
WriteSecretCore("SERVER_TRAFFIC_SECRET_0"u8, clientRandomUtf8, serverTrafficSecret0);
WriteSecretCore("CLIENT_EARLY_TRAFFIC_SECRET"u8, clientRandomUtf8, clientEarlyTrafficSecret);

s_fileStream.Flush();
}
}

private static void WriteSecretCore(ReadOnlySpan<byte> labelUtf8, ReadOnlySpan<byte> clientRandomUtf8, ReadOnlySpan<byte> secret)
{
if (secret.Length == 0)
{
return;
}

// write the secret line in the format {label} {client_random (hex)} {secret (hex)} e.g.
// SERVER_HANDSHAKE_TRAFFIC_SECRET bae582227f0f46ca663cb8c3d62e68cec38c2b947e7c4a9ec6f4e262b5ed5354 48f6bd5b0c8447d97129c6dad080f34c7f9f11ade8eeabb011f33811543411d7ab1013b1374bcd81bfface6a2deef539
int totalLength = labelUtf8.Length + 1 + clientRandomUtf8.Length + 1 + 2 * secret.Length + 1;
Comment thread
rzikm marked this conversation as resolved.
Span<byte> line = totalLength <= 1024 ? stackalloc byte[totalLength] : new byte[totalLength];

labelUtf8.CopyTo(line);
line[labelUtf8.Length] = (byte)' ';

clientRandomUtf8.CopyTo(line.Slice(labelUtf8.Length + 1));
line[labelUtf8.Length + 1 + clientRandomUtf8.Length] = (byte)' ';

HexEncode(secret, line.Slice(labelUtf8.Length + 1 + clientRandomUtf8.Length + 1));
line[^1] = (byte)'\n';

s_fileStream!.Write(line);
}

private static void HexEncode(ReadOnlySpan<byte> source, Span<byte> destination)
{
for (int i = 0; i < source.Length; i++)
{
HexConverter.ToBytesBuffer(source[i], destination.Slice(i * 2));
}
}

}
1 change: 1 addition & 0 deletions src/libraries/System.Net.Quic/src/System.Net.Quic.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
<Compile Include="$(CommonPath)System\Net\Security\TlsAlertMessage.cs" Link="Common\System\Net\Security\TlsAlertMessage.cs" />
<Compile Include="$(CommonPath)System\HexConverter.cs" Link="Common\System\HexConverter.cs" />
<Compile Include="$(CommonPath)System\Net\Security\TargetHostNameHelper.cs" Link="Common\System\Net\Security\TargetHostNameHelper.cs" />
<Compile Include="$(CommonPath)System\Net\Security\SslKeyLogger.cs" Link="Common\System\Net\Security\SslKeyLogger.cs" />
<!-- IP parser -->
<Compile Include="$(CommonPath)System\Net\IPv4AddressHelper.Common.cs" Link="System\Net\IPv4AddressHelper.Common.cs" />
<Compile Include="$(CommonPath)System\Net\IPv6AddressHelper.Common.cs" Link="System\Net\IPv6AddressHelper.Common.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#if DEBUG
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
Expand All @@ -13,14 +12,11 @@ namespace System.Net.Quic;

internal sealed class MsQuicTlsSecret : IDisposable
{
private static readonly string? s_keyLogFile = Environment.GetEnvironmentVariable("SSLKEYLOGFILE");
private static readonly FileStream? s_fileStream = s_keyLogFile != null ? File.Open(s_keyLogFile, FileMode.Append, FileAccess.Write, FileShare.ReadWrite) : null;

private unsafe QUIC_TLS_SECRETS* _tlsSecrets;

public static unsafe MsQuicTlsSecret? Create(MsQuicContextSafeHandle handle)
{
if (s_fileStream is null)
if (!SslKeyLogger.IsEnabled)
{
return null;
}
Expand Down Expand Up @@ -55,40 +51,69 @@ private unsafe MsQuicTlsSecret(QUIC_TLS_SECRETS* tlsSecrets)

public unsafe void WriteSecret()
{
Debug.Assert(_tlsSecrets is not null);
Debug.Assert(s_fileStream is not null);
ReadOnlySpan<byte> clientRandom = _tlsSecrets->IsSet.ClientRandom != 0
? new ReadOnlySpan<byte>(_tlsSecrets->ClientRandom, 32)
: ReadOnlySpan<byte>.Empty;

Span<byte> clientHandshakeTrafficSecret = _tlsSecrets->IsSet.ClientHandshakeTrafficSecret != 0
? new Span<byte>(_tlsSecrets->ClientHandshakeTrafficSecret, _tlsSecrets->SecretLength)
: Span<byte>.Empty;

Span<byte> serverHandshakeTrafficSecret = _tlsSecrets->IsSet.ServerHandshakeTrafficSecret != 0
? new Span<byte>(_tlsSecrets->ServerHandshakeTrafficSecret, _tlsSecrets->SecretLength)
: Span<byte>.Empty;

Span<byte> clientTrafficSecret0 = _tlsSecrets->IsSet.ClientTrafficSecret0 != 0
? new Span<byte>(_tlsSecrets->ClientTrafficSecret0, _tlsSecrets->SecretLength)
: Span<byte>.Empty;

lock (s_fileStream)
Span<byte> serverTrafficSecret0 = _tlsSecrets->IsSet.ServerTrafficSecret0 != 0
? new Span<byte>(_tlsSecrets->ServerTrafficSecret0, _tlsSecrets->SecretLength)
: Span<byte>.Empty;

Span<byte> clientEarlyTrafficSecret = _tlsSecrets->IsSet.ClientEarlyTrafficSecret != 0
? new Span<byte>(_tlsSecrets->ClientEarlyTrafficSecret, _tlsSecrets->SecretLength)
: Span<byte>.Empty;

SslKeyLogger.WriteSecrets(
clientRandom,
clientHandshakeTrafficSecret,
serverHandshakeTrafficSecret,
clientTrafficSecret0,
serverTrafficSecret0,
clientEarlyTrafficSecret);

// clear secrets already logged, so they are not logged again on next call,
// keep ClientRandom as it is used for all secrets (and is not a secret itself)
if (!clientHandshakeTrafficSecret.IsEmpty)
{
string clientRandom = string.Empty;
if (_tlsSecrets->IsSet.ClientRandom != 0)
{
clientRandom = Convert.ToHexString(new ReadOnlySpan<byte>(_tlsSecrets->ClientRandom, 32));
}
if (_tlsSecrets->IsSet.ClientHandshakeTrafficSecret != 0)
{
s_fileStream.Write(Encoding.ASCII.GetBytes($"CLIENT_HANDSHAKE_TRAFFIC_SECRET {clientRandom} {Convert.ToHexString(new ReadOnlySpan<byte>(_tlsSecrets->ClientHandshakeTrafficSecret, _tlsSecrets->SecretLength))}\n"));
}
if (_tlsSecrets->IsSet.ServerHandshakeTrafficSecret != 0)
{
s_fileStream.Write(Encoding.ASCII.GetBytes($"SERVER_HANDSHAKE_TRAFFIC_SECRET {clientRandom} {Convert.ToHexString(new ReadOnlySpan<byte>(_tlsSecrets->ServerHandshakeTrafficSecret, _tlsSecrets->SecretLength))}\n"));
}
if (_tlsSecrets->IsSet.ClientTrafficSecret0 != 0)
{
s_fileStream.Write(Encoding.ASCII.GetBytes($"CLIENT_TRAFFIC_SECRET_0 {clientRandom} {Convert.ToHexString(new ReadOnlySpan<byte>(_tlsSecrets->ClientTrafficSecret0, _tlsSecrets->SecretLength))}\n"));
}
if (_tlsSecrets->IsSet.ServerTrafficSecret0 != 0)
{
s_fileStream.Write(Encoding.ASCII.GetBytes($"SERVER_TRAFFIC_SECRET_0 {clientRandom} {Convert.ToHexString(new ReadOnlySpan<byte>(_tlsSecrets->ServerTrafficSecret0, _tlsSecrets->SecretLength))}\n"));
}
if (_tlsSecrets->IsSet.ClientEarlyTrafficSecret != 0)
{
s_fileStream.Write(Encoding.ASCII.GetBytes($"CLIENT_EARLY_TRAFFIC_SECRET {clientRandom} {Convert.ToHexString(new ReadOnlySpan<byte>(_tlsSecrets->ClientEarlyTrafficSecret, _tlsSecrets->SecretLength))}\n"));
}
s_fileStream.Flush();
clientHandshakeTrafficSecret.Clear();
_tlsSecrets->IsSet.ClientHandshakeTrafficSecret = 0;
}

NativeMemory.Clear(_tlsSecrets, (nuint)sizeof(QUIC_TLS_SECRETS));
if (!serverHandshakeTrafficSecret.IsEmpty)
{
serverHandshakeTrafficSecret.Clear();
_tlsSecrets->IsSet.ServerHandshakeTrafficSecret = 0;
}

if (!clientTrafficSecret0.IsEmpty)
{
clientTrafficSecret0.Clear();
_tlsSecrets->IsSet.ClientTrafficSecret0 = 0;
}

if (!serverTrafficSecret0.IsEmpty)
{
serverTrafficSecret0.Clear();
_tlsSecrets->IsSet.ServerTrafficSecret0 = 0;
}

if (!clientEarlyTrafficSecret.IsEmpty)
{
clientEarlyTrafficSecret.Clear();
_tlsSecrets->IsSet.ClientEarlyTrafficSecret = 0;
}
}

public unsafe void Dispose()
Expand All @@ -110,4 +135,3 @@ public unsafe void Dispose()
}
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -187,13 +187,11 @@ static async ValueTask<QuicConnection> StartConnectAsync(QuicClientConnectionOpt
/// </summary>
private SslApplicationProtocol _negotiatedApplicationProtocol;

#if DEBUG
/// <summary>
/// Will contain TLS secret after CONNECTED event is received and store it into SSLKEYLOGFILE.
/// MsQuic holds the underlying pointer so this object can be disposed only after connection native handle gets closed.
/// </summary>
private readonly MsQuicTlsSecret? _tlsSecret;
#endif

/// <summary>
/// The remote endpoint used for this connection.
Expand Down Expand Up @@ -254,9 +252,7 @@ private unsafe QuicConnection()
throw;
}

#if DEBUG
_tlsSecret = MsQuicTlsSecret.Create(_handle);
#endif
}

/// <summary>
Expand Down Expand Up @@ -284,9 +280,7 @@ internal unsafe QuicConnection(QUIC_HANDLE* handle, QUIC_NEW_CONNECTION_INFO* in

_remoteEndPoint = MsQuicHelpers.QuicAddrToIPEndPoint(info->RemoteAddress);
_localEndPoint = MsQuicHelpers.QuicAddrToIPEndPoint(info->LocalAddress);
#if DEBUG
_tlsSecret = MsQuicTlsSecret.Create(_handle);
#endif
}

private async ValueTask FinishConnectAsync(QuicClientConnectionOptions options, CancellationToken cancellationToken = default)
Expand Down Expand Up @@ -510,9 +504,8 @@ private unsafe int HandleEventConnected(ref CONNECTED_DATA data)
QuicAddr localAddress = MsQuicHelpers.GetMsQuicParameter<QuicAddr>(_handle, QUIC_PARAM_CONN_LOCAL_ADDRESS);
_localEndPoint = MsQuicHelpers.QuicAddrToIPEndPoint(&localAddress);

#if DEBUG
// Final (1-RTT) secrets have been derived, log them if desired to allow decrypting application traffic.
_tlsSecret?.WriteSecret();
#endif

if (NetEventSource.Log.IsEnabled())
{
Expand All @@ -535,6 +528,9 @@ private unsafe int HandleEventShutdownInitiatedByPeer(ref SHUTDOWN_INITIATED_BY_
}
private unsafe int HandleEventShutdownComplete()
{
// make sure we log at least some secrets in case of shutdown before handshake completes.
_tlsSecret?.WriteSecret();

Exception exception = ExceptionDispatchInfo.SetCurrentStackTrace(_disposed == 1 ? new ObjectDisposedException(GetType().FullName) : ThrowHelper.GetOperationAbortedException());
_acceptQueue.Writer.TryComplete(exception);
_connectedTcs.TrySetException(exception);
Expand Down Expand Up @@ -578,6 +574,9 @@ private unsafe int HandleEventPeerCertificateReceived(ref PEER_CERTIFICATE_RECEI
// worker threads.
//

// Handshake keys should be available by now, log them now if desired.
_tlsSecret?.WriteSecret();

var task = _sslConnectionOptions.StartAsyncCertificateValidation((IntPtr)data.Certificate, (IntPtr)data.Chain);
if (task.IsCompletedSuccessfully)
{
Expand Down
Loading