dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
14.57k stars 4.55k forks source link

sslStream.NegotiatedApplicationProtocol is empty after TLS1.3 renegotiation on Windows #102731

Closed tiagonapoli closed 1 month ago

tiagonapoli commented 1 month ago

Description

On Windows, in the first client-side read after the TLS1.3 handshake, a renegotiation happens in the client side, which is expected for Schannel on TLS1.3. However, the renegotiation is overwritting sslStream.NegotiatedApplicationProtocol with an empty value.

After the renegotiation happens, ProcessHandshakeSuccess is called, which will re-populate connectionInfo. However, when it gets the negotiated application protocol using SSPIWrapper.QueryBlittableContextAttributes(GlobalSSPI.SSPISecureChannel, context, Interop.SspiCli.ContextAttribute.SECPKG_ATTR_APPLICATION_PROTOCOL, ref alpnContext), it returns empty.

https://github.com/dotnet/runtime/blob/2feae23741a44e31e0e0de5d43e0a5919688505e/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Protocol.cs#L971C1-L996C10

internal void ProcessHandshakeSuccess()
{
    //...
    SslStreamPal.QueryContextConnectionInfo(_securityContext!, ref _connectionInfo);
    // ...
}

https://github.com/dotnet/runtime/blob/2feae23741a44e31e0e0de5d43e0a5919688505e/src/libraries/System.Net.Security/src/System/Net/Security/SslConnectionInfo.Windows.cs#L11C1-L38C10

private static byte[]? GetNegotiatedApplicationProtocol(SafeDeleteContext context)
{
    Interop.SecPkgContext_ApplicationProtocol alpnContext = default;
    bool success = SSPIWrapper.QueryBlittableContextAttributes(GlobalSSPI.SSPISecureChannel, context, Interop.SspiCli.ContextAttribute.SECPKG_ATTR_APPLICATION_PROTOCOL, ref alpnContext);

    // Check if the context returned is alpn data, with successful negotiation.
    if (success &&
        alpnContext.ProtoNegoExt == Interop.ApplicationProtocolNegotiationExt.ALPN &&
        alpnContext.ProtoNegoStatus == Interop.ApplicationProtocolNegotiationStatus.Success)
    {
        if (alpnContext.Protocol.SequenceEqual(s_http1))
        {
            return s_http1;
        }
        else if (alpnContext.Protocol.SequenceEqual(s_http2))
        {
            return s_http2;
        }
        else if (alpnContext.Protocol.SequenceEqual(s_http3))
        {
            return s_http3;
        }

        return alpnContext.Protocol.ToArray();
    }

    return null;
}

Reproduction Steps

Client code:

class Program
{
    static void Main(string[] args)
    {
        TcpClient client = new TcpClient("127.0.0.1", 5300);
        var stream = client.GetStream();
        SslStream sslStream = new SslStream(stream, false, new RemoteCertificateValidationCallback(CertificateValidationCallback));
        SslClientAuthenticationOptions options = new SslClientAuthenticationOptions
        {
            TargetHost = "clientName",
            ClientCertificates = null,
            EnabledSslProtocols = System.Security.Authentication.SslProtocols.Tls13,
            CertificateRevocationCheckMode = X509RevocationMode.NoCheck,
            ApplicationProtocols = new List<SslApplicationProtocol> { SslApplicationProtocol.Http2 }
        };

        sslStream.AuthenticateAsClient(options);
        Console.WriteLine("Finished SSL handshake\n");
        string negotiatedProtocol = Encoding.ASCII.GetString(sslStream.NegotiatedApplicationProtocol.Protocol.Span);
        Console.WriteLine($"[Before sslStream.Read] Negotiated protocol: {negotiatedProtocol}[len={sslStream.NegotiatedApplicationProtocol.Protocol.Span.Length}]");

        byte[] buffer = new byte[client.ReceiveBufferSize];
        while (true)
        {
            int bytesRead = sslStream.Read(buffer, 0, client.ReceiveBufferSize);
            negotiatedProtocol = Encoding.ASCII.GetString(sslStream.NegotiatedApplicationProtocol.Protocol.Span);
            Console.WriteLine($"[After sslStream.Read] Negotiated protocol: {negotiatedProtocol}[len={sslStream.NegotiatedApplicationProtocol.Protocol.Span.Length}]");
            Console.WriteLine(Encoding.ASCII.GetString(buffer, 0, bytesRead));
            Task.Delay(1000).Wait();
        }
    }

    //Callback function that allows all certificates
    static bool CertificateValidationCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
    {
        return true;
    }
}

The output in the client side:

Finished SSL handshake

[Before sslStream.Read] Negotiated protocol: h2[len=2]
[After sslStream.Read] Negotiated protocol: [len=0]
message from server

Server code:

class Program
{
    static void Main(string[] args)
    {
        var certificate = new X509Certificate2("server.pfx", "...");

        //Starts Tcp Listener
        var listener = new TcpListener(IPAddress.Loopback, 5300);
        listener.Start();
        var client = listener.AcceptTcpClient();
        var stream = client.GetStream();
        SslStream sslStream = new SslStream(stream, false);

        SslServerAuthenticationOptions options = new SslServerAuthenticationOptions
        {
            ServerCertificate = certificate,
            ClientCertificateRequired = false,
            EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13,
            CertificateRevocationCheckMode = X509RevocationMode.NoCheck,
            ApplicationProtocols = new System.Collections.Generic.List<SslApplicationProtocol> { SslApplicationProtocol.Http2 }
        };

        sslStream.AuthenticateAsServer(options);

        Console.WriteLine("Finished SSL handshake\n");
        string negotiatedProtocol = Encoding.ASCII.GetString(sslStream.NegotiatedApplicationProtocol.Protocol.Span);
        Console.WriteLine("Negotiated protocol: " + negotiatedProtocol);

        byte[] buffer = new byte[client.ReceiveBufferSize];
        while (client.Connected)
        {
            string message = "message from server\n";
            sslStream.Write(Encoding.UTF8.GetBytes(message), 0, message.Length);
            sslStream.Flush();
            Console.WriteLine("Wrote message to client with size " + message.Length);

            negotiatedProtocol = Encoding.ASCII.GetString(sslStream.NegotiatedApplicationProtocol.Protocol.Span);
            Console.WriteLine("Negotiated protocol: " + negotiatedProtocol);
            Task.Delay(20000).Wait();
        }
    }
}

Expected behavior

Negotiated protocol should be http2 after first client-side read (this is the behavior with TLS1.2).

Actual behavior

Negotiated protocol is empty after first client-side read.

Regression?

No response

Known Workarounds

No response

Configuration

.NET SDK: Version: 8.0.205 Commit: 3e1383b780 Workload version: 8.0.200-manifests.818b3449

OS Name: Microsoft Windows 11 Enterprise OS Version: 10.0.22631 N/A Build 22631 Arch: x64

Other information

No response

dotnet-policy-service[bot] commented 1 month ago

Tagging subscribers to this area: @dotnet/ncl, @bartonjs, @vcsjones See info in area-owners.md if you want to be subscribed.