diff --git a/src/libraries/System.Net.Quic/src/System.Net.Quic.csproj b/src/libraries/System.Net.Quic/src/System.Net.Quic.csproj
index c30e10521a02f..53b39492c39a4 100644
--- a/src/libraries/System.Net.Quic/src/System.Net.Quic.csproj
+++ b/src/libraries/System.Net.Quic/src/System.Net.Quic.csproj
@@ -32,6 +32,7 @@
+
diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/MsQuicSafeHandle.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/MsQuicSafeHandle.cs
index 3adc922745423..8e87654f2e321 100644
--- a/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/MsQuicSafeHandle.cs
+++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/MsQuicSafeHandle.cs
@@ -92,6 +92,23 @@ internal sealed class MsQuicContextSafeHandle : MsQuicSafeHandle
///
private readonly MsQuicSafeHandle? _parent;
+#if DEBUG
+ ///
+ /// Native memory to hold TLS secrets. It needs to live same cycle as the underlying connection.
+ ///
+ private unsafe QUIC_TLS_SECRETS* _tlsSecrets;
+
+ public unsafe QUIC_TLS_SECRETS* GetSecretsBuffer()
+ {
+ if (_tlsSecrets == null)
+ {
+ _tlsSecrets = (QUIC_TLS_SECRETS*)NativeMemory.Alloc((nuint)sizeof(QUIC_TLS_SECRETS));
+ }
+
+ return _tlsSecrets;
+ }
+#endif
+
public unsafe MsQuicContextSafeHandle(QUIC_HANDLE* handle, GCHandle context, SafeHandleType safeHandleType, MsQuicSafeHandle? parent = null)
: base(handle, safeHandleType)
{
@@ -108,7 +125,7 @@ public unsafe MsQuicContextSafeHandle(QUIC_HANDLE* handle, GCHandle context, Saf
}
}
- protected override bool ReleaseHandle()
+ protected override unsafe bool ReleaseHandle()
{
base.ReleaseHandle();
if (_context.IsAllocated)
@@ -123,6 +140,13 @@ protected override bool ReleaseHandle()
NetEventSource.Info(this, $"{this} {_parent} ref count decremented");
}
}
+#if DEBUG
+ if (_tlsSecrets != null)
+ {
+ NativeMemory.Clear(_tlsSecrets, (nuint)sizeof(QUIC_TLS_SECRETS));
+ NativeMemory.Free(_tlsSecrets);
+ }
+#endif
return true;
}
}
diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/MsQuicTlsSecret.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/MsQuicTlsSecret.cs
new file mode 100644
index 0000000000000..3a19656f63acb
--- /dev/null
+++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/MsQuicTlsSecret.cs
@@ -0,0 +1,109 @@
+// 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.Collections.Generic;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading;
+using Microsoft.Quic;
+using static Microsoft.Quic.MsQuic;
+
+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) : null;
+
+ private unsafe QUIC_TLS_SECRETS* _tlsSecrets;
+
+ public static unsafe MsQuicTlsSecret? Create(MsQuicContextSafeHandle handle)
+ {
+ if (s_fileStream != null)
+ {
+ try
+ {
+ QUIC_TLS_SECRETS* ptr = handle.GetSecretsBuffer();
+ if (ptr != null)
+ {
+ int status = MsQuicApi.Api.SetParam(handle, QUIC_PARAM_CONN_TLS_SECRETS, (uint)sizeof(QUIC_TLS_SECRETS), ptr);
+
+ if (StatusSucceeded(status))
+ {
+ return new MsQuicTlsSecret(ptr);
+ }
+ else
+ {
+ if (NetEventSource.Log.IsEnabled())
+ {
+ NetEventSource.Error(handle, "Failed to set native memory for TLS secret.");
+ }
+ }
+ }
+ }
+ catch { };
+ }
+
+ return null;
+ }
+
+ private unsafe MsQuicTlsSecret(QUIC_TLS_SECRETS* memory)
+ {
+ _tlsSecrets = memory;
+ }
+
+ public void WriteSecret() => WriteSecret(s_fileStream);
+ public unsafe void WriteSecret(FileStream? stream)
+ {
+ if (stream != null && _tlsSecrets != null)
+ {
+ lock (stream)
+ {
+ string clientRandom = string.Empty;
+
+ if (_tlsSecrets->IsSet.ClientRandom != 0)
+ {
+ clientRandom = HexConverter.ToString(new ReadOnlySpan(_tlsSecrets->ClientRandom, 32));
+ }
+
+ if (_tlsSecrets->IsSet.ClientHandshakeTrafficSecret != 0)
+ {
+ stream.Write(Encoding.ASCII.GetBytes($"CLIENT_HANDSHAKE_TRAFFIC_SECRET {clientRandom} {HexConverter.ToString(new ReadOnlySpan(_tlsSecrets->ClientHandshakeTrafficSecret, _tlsSecrets->SecretLength))}\n"));
+ }
+
+ if (_tlsSecrets->IsSet.ServerHandshakeTrafficSecret != 0)
+ {
+ stream.Write(Encoding.ASCII.GetBytes($"SERVER_HANDSHAKE_TRAFFIC_SECRET {clientRandom} {HexConverter.ToString(new ReadOnlySpan(_tlsSecrets->ServerHandshakeTrafficSecret, _tlsSecrets->SecretLength))}\n"));
+ }
+
+ if (_tlsSecrets->IsSet.ClientTrafficSecret0 != 0)
+ {
+ stream.Write(Encoding.ASCII.GetBytes($"CLIENT_TRAFFIC_SECRET_0 {clientRandom} {HexConverter.ToString(new ReadOnlySpan(_tlsSecrets->ClientTrafficSecret0, _tlsSecrets->SecretLength))}\n"));
+ }
+
+ if (_tlsSecrets->IsSet.ServerTrafficSecret0 != 0)
+ {
+ stream.Write(Encoding.ASCII.GetBytes($"SERVER_TRAFFIC_SECRET_0 {clientRandom} {HexConverter.ToString(new ReadOnlySpan(_tlsSecrets->ServerTrafficSecret0, _tlsSecrets->SecretLength))}\n"));
+ }
+
+ if (_tlsSecrets->IsSet.ClientEarlyTrafficSecret != 0)
+ {
+ stream.Write(Encoding.ASCII.GetBytes($"CLIENT_EARLY_TRAFFIC_SECRET {clientRandom} {HexConverter.ToString(new ReadOnlySpan(_tlsSecrets->ClientEarlyTrafficSecret, _tlsSecrets->SecretLength))}\n"));
+ }
+
+ stream.Flush();
+ }
+ }
+ }
+
+ public unsafe void Dispose()
+ {
+ if (_tlsSecrets != null)
+ {
+ NativeMemory.Clear(_tlsSecrets, (nuint)sizeof(QUIC_TLS_SECRETS));
+ }
+ }
+}
+#endif
diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.cs
index 4422277733425..b3ac83567cee4 100644
--- a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.cs
+++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.cs
@@ -38,6 +38,13 @@ namespace System.Net.Quic;
///
public sealed partial class QuicConnection : IAsyncDisposable
{
+#if DEBUG
+ ///
+ /// The actual secret structure wrapper passed to MsQuic.
+ ///
+ private MsQuicTlsSecret? _tlsSecret;
+#endif
+
///
/// Returns true if QUIC is supported on the current machine and can be used; otherwise, false.
///
@@ -145,7 +152,6 @@ static async ValueTask StartConnectAsync(QuicClientConnectionOpt
/// Set when CONNECTED is received.
///
private SslApplicationProtocol _negotiatedApplicationProtocol;
-
///
/// The remote endpoint used for this connection.
///
@@ -204,6 +210,10 @@ private unsafe QuicConnection()
context.Free();
throw;
}
+
+#if DEBUG
+ _tlsSecret = MsQuicTlsSecret.Create(_handle);
+#endif
}
///
@@ -231,6 +241,9 @@ internal unsafe QuicConnection(QUIC_HANDLE* handle, QUIC_NEW_CONNECTION_INFO* in
_remoteEndPoint = info->RemoteAddress->ToIPEndPoint();
_localEndPoint = info->LocalAddress->ToIPEndPoint();
+#if DEBUG
+ _tlsSecret = MsQuicTlsSecret.Create(_handle);
+#endif
}
private async ValueTask FinishConnectAsync(QuicClientConnectionOptions options, CancellationToken cancellationToken = default)
@@ -600,6 +613,9 @@ public async ValueTask DisposeAsync()
return;
}
+#if DEBUG
+ _tlsSecret?.Dispose();
+#endif
// Check if the connection has been shut down and if not, shut it down.
if (_shutdownTcs.TryInitialize(out ValueTask valueTask, this))
{