Skip to content

TlsSession.SetRemoteCertificateValidationResult rejection message ends in a literal {0} #135349

Description

@caraioniurie47

Description

TlsSession.SetRemoteCertificateValidationResult builds its rejection exception from the net_ssl_io_cert_validation resource without SR.Format, so the exception message ends in a literal {0} instead of the SslPolicyErrors value the caller passed.

TlsSession.cs:490-492

                if (_isHandshakeComplete)
                {
                    _externalValidationFault = new AuthenticationException(SR.net_ssl_io_cert_validation);

TlsSession.cs:506

                    _externalValidationFault = new AuthenticationException(SR.net_ssl_io_cert_validation);

The resource has a placeholder:

Strings.resx:218-219

  <data name="net_ssl_io_cert_validation" xml:space="preserve">
    <value>The remote certificate is invalid according to the validation procedure: {0}</value>

SslStream formats the same resource with its SslPolicyErrors:

SslStream.IO.cs:710

                return ExceptionDispatchInfo.SetCurrentStackTrace(new AuthenticationException(SR.Format(SR.net_ssl_io_cert_validation, sslPolicyErrors), null));

Reproduction Steps

A server TlsBufferSession with ClientCertificateRequired = true and no RemoteCertificateValidationCallback rejects the client certificate with SetRemoteCertificateValidationResult(SslPolicyErrors.RemoteCertificateChainErrors). The client is an SslStream over loopback TCP. The program prints the message of the AuthenticationException the session throws, once for TLS 1.2 and once for TLS 1.3.

repro.csproj:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net11.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <NoWarn>$(NoWarn);SYSLIB5007</NoWarn>
  </PropertyGroup>
</Project>

Program.cs:

using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Authentication;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;

foreach (SslProtocols protocol in new[] { SslProtocols.Tls12, SslProtocols.Tls13 })
{
    Console.WriteLine($"{protocol}: {await RejectClientCertificateAsync(protocol)}");
}

static async Task<string> RejectClientCertificateAsync(SslProtocols protocol)
{
    using X509Certificate2 serverCert = CreateCertificate("CN=localhost");
    using X509Certificate2 clientCert = CreateCertificate("CN=client");

    using var listener = new TcpListener(IPAddress.Loopback, 0);
    listener.Start();
    using var clientTcp = new TcpClient();
    await clientTcp.ConnectAsync(IPAddress.Loopback, ((IPEndPoint)listener.LocalEndpoint).Port);
    using TcpClient serverTcp = await listener.AcceptTcpClientAsync();
    NetworkStream transport = serverTcp.GetStream();

    using var clientSsl = new SslStream(clientTcp.GetStream(), false, (_, _, _, _) => true);
    _ = clientSsl.AuthenticateAsClientAsync(new SslClientAuthenticationOptions
    {
        TargetHost = "localhost",
        EnabledSslProtocols = protocol,
        ClientCertificates = new X509CertificateCollection { clientCert },
    });

    // No RemoteCertificateValidationCallback: the caller validates the client certificate.
    using TlsContext context = TlsContext.CreateServer(new SslServerAuthenticationOptions
    {
        ServerCertificate = serverCert,
        EnabledSslProtocols = protocol,
        ClientCertificateRequired = true,
    });
    using var session = new TlsBufferSession();
    session.SetContext(context);

    byte[] input = new byte[32 * 1024];
    byte[] output = new byte[32 * 1024];
    int inputLength = 0;
    try
    {
        while (!session.IsHandshakeComplete)
        {
            TlsOperationStatus status = session.Handshake(input.AsSpan(0, inputLength), output, out int consumed, out int written);
            input.AsSpan(consumed, inputLength - consumed).CopyTo(input);
            inputLength -= consumed;
            await transport.WriteAsync(output.AsMemory(0, written));
            while (session.HasPendingOutput)
            {
                session.DrainPendingOutput(output, out written);
                await transport.WriteAsync(output.AsMemory(0, written));
            }

            if (status == TlsOperationStatus.NeedsCertificateValidation)
            {
                session.SetRemoteCertificateValidationResult(SslPolicyErrors.RemoteCertificateChainErrors);
            }
            else if (status == TlsOperationStatus.NeedMoreData)
            {
                int read = await transport.ReadAsync(input.AsMemory(inputLength));
                if (read == 0)
                {
                    return "peer closed the connection";
                }
                inputLength += read;
            }
        }

        session.Write("hello"u8, output, out _, out _);
        return "no exception";
    }
    catch (AuthenticationException ex)
    {
        return $"{ex.GetType().Name}: {ex.Message}";
    }
}

static X509Certificate2 CreateCertificate(string subject)
{
    using RSA key = RSA.Create(2048);
    var request = new CertificateRequest(subject, key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
    using X509Certificate2 certificate = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddDays(1));
    return X509CertificateLoader.LoadPkcs12(certificate.Export(X509ContentType.Pfx), null);
}

Build and run:

dotnet build -c Release -o out
dotnet out/repro.dll

Expected behavior

The message names the errors the caller passed in, as SslStream's message does:

Tls12: AuthenticationException: The remote certificate is invalid according to the validation procedure: RemoteCertificateChainErrors
Tls13: AuthenticationException: The remote certificate is invalid according to the validation procedure: RemoteCertificateChainErrors

Actual behavior

On .NET 11.0.0-rc.1.26420.103:

Tls12: AuthenticationException: The remote certificate is invalid according to the validation procedure: {0}
Tls13: AuthenticationException: The remote certificate is invalid according to the validation procedure: {0}

Regression?

No. Both lines came in with the commit that added TlsContext and TlsSession (#130366), and main at d148189 still has them unchanged.

Known Workarounds

The caller has the SslPolicyErrors value it passed to SetRemoteCertificateValidationResult and can log that value itself.

Configuration

.NET 11.0.0-rc.1.26420.103, Ubuntu 24.04.4 LTS, x64, OpenSSL 3.0.13. A local build of main at 6f1d933 prints the same {0} lines.

The repro was run on that Linux machine only. The code is not OpenSSL-specific: TlsSession.cs is compiled for every platform target except Android, Windows included:

System.Net.Security.csproj:80-81

    <Compile Include="System\Net\Security\TlsSession.cs"
             Condition="'$(TargetPlatformIdentifier)' != '' and '$(UseAndroidCrypto)' != 'true'" />

Other information

The fix is SR.Format(SR.net_ssl_io_cert_validation, errors) at both sites. In the repro on Linux (OpenSSL 3.0.13), both TLS 1.2 and TLS 1.3 reach the first site (the _isHandshakeComplete branch); the second site (the _resumeAfterCertValidation branch) was not reached.

On Windows 11 x64 the regression test in the fix's PR fails on the old code too: for TLS 1.2 and TLS 1.3, the server-side message does not contain RemoteCertificateChainErrors.

I have a fix with a test ready and will open a PR for it shortly. Could this be assigned to me?

Note

AI-generated, written at my direction and reviewed by me before posting.
Source read at dotnet/runtime 6f1d933. The repro ran on Ubuntu 24.04.4 LTS x64 with OpenSSL 3.0.13, on .NET 11.0.0-rc.1.26420.103 and on a ./build.sh clr+libs -rc Release build of 6f1d933, with dotnet build -c Release -o out and dotnet out/repro.dll (the local build through its testhost dotnet with DOTNET_ROLL_FORWARD=LatestMajor).
The Windows result is from that test, run on Windows 11 Pro for Workstations x64 (10.0.26300), against a build.cmd clr+libs -rc checked build of dotnet/runtime d148189 (same TlsSession.cs as 6f1d933), run with dotnet.cmd build /t:Test in src/libraries/System.Net.Security/tests/FunctionalTests.

Activity

  1. dotnet-policy-service commented on Oct 7, 2026

    @dotnet-policy-service
    Contributor

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

  2. removed
    untriagedNew issue has not been triaged by the area owner
    on Oct 7, 2026
  3. added this to the 12.0.0 milestone on Oct 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions