Skip to content

Support SSLKEYLOGFILE in Release builds #100665

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Apr 10, 2024
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
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;
#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;
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