Skip to content

Change HttpClient/SslStream default certificate revocation check mode to Online #116098

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

Conversation

rzikm
Copy link
Member

@rzikm rzikm commented May 29, 2025

Set the default certificate revocation mode to online, enhancing security by ensuring that certificate revocation checks are performed by default.

This is a breaking change, which can be opted out by either explicitly specifying X509RevocationMode.NoCheck in code, or by setting the opt-out appctx switch:

  • System.Net.Security.NoRevocationCheckByDefault appctx switch to true
  • DOTNET_SYSTEM_NET_SECURITY_NOREVOCATIONCHECKBYDEFAULT environment variable to true

This PR also changes the tests to make sure the corresponding RevocationResponsder is alive and available while we use dynamically generated certificates during the test runs.

@stephentoub stephentoub added the breaking-change Issue or PR that represents a breaking API or functional change over a prerelease. label May 29, 2025
Copy link
Contributor

dotnet-policy-service bot commented May 29, 2025

Added needs-breaking-change-doc-created label because this PR has the breaking-change label.

When you commit this breaking change:

  1. Create and link to this PR and the issue a matching issue in the dotnet/docs repo using the breaking change documentation template, then remove this needs-breaking-change-doc-created label.
  2. Ask a committer to mail the .NET Breaking Change Notification DL.

Tagging @dotnet/compat for awareness of the breaking change.

@rzikm rzikm marked this pull request as ready for review June 5, 2025 14:03
@rzikm rzikm requested review from Copilot, a team, bartonjs, blowdart and GrabYourPitchforks June 5, 2025 14:03
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR changes the default certificate revocation mode from NoCheck to Online (breaking change), introduces an AppContext switch for opt-out, and updates tests to explicitly set the revocation mode and to use a new PkiHolder helper for certificate lifecycle management.

  • Introduce SslAuthenticationOptions.DefaultRevocationMode driven by the OfflineRevocationByDefault switch and apply it as the new default in client/server options.
  • Update unit and functional tests to explicitly set CertificateRevocationCheckMode to NoCheck where needed and refactor certificate generation/cleanup to use PkiHolder.
  • Adjust HTTP handlers and WinHTTP defaults/tests to reflect the new revocation-list default.

Reviewed Changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated no comments.

Show a summary per file
File Description
src/**/SslAuthenticationOptions.cs Add DefaultRevocationMode and hook into AppContext switch
src/**/SslServerAuthenticationOptions.cs & SslClientAuthenticationOptions.cs Change private _checkCertificateRevocation to use the new default
src/**/System.Net.Security.csproj & UnitTests.csproj Include AppContextSwitchHelper in build
src//tests//* Update tests for explicit revocation settings and refactor certificate setup
src/**/WinHttpHandler.cs & WinHttpHandlerTest.cs Set default CheckCertificateRevocationList = true and tests
src/**/HttpClientHandlerTest*.cs Update default assertions for revocation list
src/**/Configuration.Certificates.Dynamic.cs Replace manual cert management with new PkiHolder class

Comment on lines 16 to 20
internal static readonly X509RevocationMode DefaultRevocationMode =
AppContextSwitchHelper.GetBooleanConfig(
"System.Net.Security.OfflineRevocationByDefault",
"DOTNET_SYSTEM_NET_SECURITY_OFFLINEREVOCATIONBYDEFAULT")
? X509RevocationMode.NoCheck : X509RevocationMode.Online;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added

@@ -68,7 +68,7 @@ private Func<
X509Chain,
SslPolicyErrors,
bool>? _serverCertificateValidationCallback;
private bool _checkCertificateRevocationList;
private bool _checkCertificateRevocationList = true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be true, or should it inspect the appcontext-overridable default mode value?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't see from the PR, so: Ensure you have tests where this gets forced to false, instead of only tests that assumed it was false. Ideally tests where the cert is revoked (or the revocation responder is set to explode, or time out, or whatever), so you can tell it worked.

@wfurt
Copy link
Member

wfurt commented Jun 5, 2025

I don't think we should go forward with. It seems like the can have broad and breaking impact so as it can impact perf and reliability. I would much rather update best practices to encourage use where appropriate

@@ -11,7 +11,7 @@ namespace System.Net.Security
public class SslClientAuthenticationOptions
{
private EncryptionPolicy _encryptionPolicy = EncryptionPolicy.RequireEncryption;
private X509RevocationMode _checkCertificateRevocation = X509RevocationMode.NoCheck;
private X509RevocationMode _checkCertificateRevocation = SslAuthenticationOptions.DefaultRevocationMode;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like with the WinHttpHandler, double check that you have tests explicitly trying to use NoCheck, rather than just assuming it as a default.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, and there are also HttpClient tests which do the same (so WinHttpHandler is also covered)

Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@rzikm rzikm requested a review from bartonjs June 9, 2025 10:12
@rzikm
Copy link
Member Author

rzikm commented Jun 9, 2025

/azp run runtime-extra-platforms

Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@rzikm
Copy link
Member Author

rzikm commented Jun 10, 2025

/ba-g test failure (System.Runtime.Tests) is unrelated, other failures are known

@karelz karelz added this to the 10.0.0 milestone Jun 10, 2025
@filipnavara
Copy link
Member

Heads up: LetsEncrypt recently deprecated OCSP in favor of CRL. Apple platforms tend to run into issues of not checking the CRL (#25872) and returning 509ChainStatusFlags.RevocationStatusUnknown error. You may want to check this change on some recent Apple macOS machine first against a server with recent LetsEncrypt certificate (eg. http://smartermail.emclient.com/).

Copy link
Member

@rokonec rokonec left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@rzikm
Copy link
Member Author

rzikm commented Jun 13, 2025

Benchmark results comparing handshake time with and without revocation checking (they can be easily run on .NET 9)

Benchmark code
BenchmarkDotNet.Running.BenchmarkRunner.Run<RevocationCheckingBenchmark>();

public class RevocationCheckingBenchmark
{
    private PkiHolder _pkiHolder;

    private PipeStream _clientPipe, _serverPipe;    // used for handshake tests
    private NetworkStream _clientIPv4, _serverIPv4; // used for handshake tests
    private readonly byte[] _clientBuffer = new byte[1], _serverBuffer = new byte[1];

    [GlobalSetup]
    public void Setup()
    {
        string pipeName = "SetupTlsHandshakePipe";
        using (var listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
        {
            listener.Bind(new IPEndPoint(IPAddress.Loopback, 0));
            listener.Listen(1);

            // Create an SslStream pair
            var client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            client.Connect(listener.LocalEndPoint);
            Socket server = listener.Accept();

            // Create a IPv4 pair
            client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            client.Connect(listener.LocalEndPoint);
            server = listener.Accept();
            _clientIPv4 = new NetworkStream(client, ownsSocket: true);
            _serverIPv4 = new NetworkStream(server, ownsSocket: true);
        }

        // Create PIPE Pair.
        var pipeServer = new NamedPipeServerStream(pipeName, PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous | PipeOptions.WriteThrough);
        var pipeClient = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, PipeOptions.Asynchronous | PipeOptions.WriteThrough);
        Task.WaitAll(pipeServer.WaitForConnectionAsync(), pipeClient.ConnectAsync());
        _serverPipe = pipeServer;
        _clientPipe = pipeClient;
        _pkiHolder = CertificateAuthority.BuildPrivatePki(PkiOptions, nameof(RevocationCheckingBenchmark), subjectName: "Test");
    }

    [GlobalCleanup]
    public void Cleanup()
    {
        _clientPipe.Dispose();
        _serverPipe.Dispose();
        _pkiHolder.Dispose();
    }

    [Params(PkiOptions.AllRevocation, PkiOptions.CrlEverywhere, PkiOptions.OcspEverywhere)]
    public PkiOptions PkiOptions;

    [Params(X509RevocationMode.NoCheck, X509RevocationMode.Online)]
    public X509RevocationMode RevocationMode;

    [Benchmark]
    [IterationCount(10)]
    public Task DefaultHandshakePipeAsync() => DefaultHandshake(_clientIPv4, _serverIPv4, RevocationMode);

    private async Task DefaultHandshake(Stream client, Stream server, X509RevocationMode revocationMode, bool requireClientCert = false)
    {
        SslClientAuthenticationOptions clientOptions = new SslClientAuthenticationOptions
        {
            AllowRenegotiation = false,
            CertificateRevocationCheckMode = revocationMode,
            TargetHost = Guid.NewGuid().ToString(), // Use a random target host to avoid DNS issues
            AllowTlsResume = false,
        };

        SslServerAuthenticationOptions serverOptions = new SslServerAuthenticationOptions
        {
            AllowRenegotiation = false,
            CertificateRevocationCheckMode = revocationMode,
            ServerCertificateContext = _pkiHolder.CreateSslStreamCertificateContext(),
            ClientCertificateRequired = requireClientCert,
            AllowTlsResume = false,
        };

        using (var sslClient = new SslStream(client, leaveInnerStreamOpen: true, delegate { return true; }))
        using (var sslServer = new SslStream(server, leaveInnerStreamOpen: true, delegate { return true; }))
        using (CancellationTokenSource cts = new CancellationTokenSource())
        {
            cts.CancelAfter(TimeSpan.FromSeconds(10));
            await Task.WhenAll(
                sslClient.AuthenticateAsClientAsync(clientOptions, cts.Token),
                sslServer.AuthenticateAsServerAsync(serverOptions, cts.Token));

            // In Tls1.3 part of handshake happens with data exchange.
            await sslClient.WriteAsync(_clientBuffer, cts.Token);
#pragma warning disable CA2022 // Avoid inexact read
            await sslServer.ReadAsync(_serverBuffer, cts.Token);
            await sslServer.WriteAsync(_serverBuffer, cts.Token);
            await sslClient.ReadAsync(_clientBuffer, cts.Token);
#pragma warning restore CA2022
        }
    }
}
BenchmarkDotNet v0.14.0, Windows 11 (10.0.26100.4349)
Intel Core i9-10900K CPU 3.70GHz, 1 CPU, 20 logical and 10 physical cores
.NET SDK 9.0.301
  [Host]     : .NET 9.0.6 (9.0.625.26613), X64 RyuJIT AVX2
  Job-VODIEA : .NET 9.0.6 (9.0.625.26613), X64 RyuJIT AVX2

IterationCount=10

| Method                    | PkiOptions     | RevocationMode | Mean     | Error     | StdDev    |
|-------------------------- |--------------- |--------------- |---------:|----------:|----------:|
| DefaultHandshakePipeAsync | CrlEverywhere  | NoCheck        | 9.765 ms | 0.2233 ms | 0.1329 ms |
| DefaultHandshakePipeAsync | CrlEverywhere  | Online         | 3.397 ms | 0.1072 ms | 0.0709 ms |
| DefaultHandshakePipeAsync | OcspEverywhere | NoCheck        | 3.328 ms | 0.0526 ms | 0.0348 ms |
| DefaultHandshakePipeAsync | OcspEverywhere | Online         | 3.328 ms | 0.0428 ms | 0.0283 ms |
| DefaultHandshakePipeAsync | AllRevocation  | NoCheck        | 3.379 ms | 0.0924 ms | 0.0611 ms |
| DefaultHandshakePipeAsync | AllRevocation  | Online         | 3.323 ms | 0.0579 ms | 0.0345 ms |

BenchmarkDotNet v0.14.0, Ubuntu 24.04.2 LTS (Noble Numbat) WSL
Intel Core i9-10900K CPU 3.70GHz, 1 CPU, 20 logical and 10 physical cores
.NET SDK 9.0.300
  [Host]     : .NET 9.0.5 (9.0.525.21509), X64 RyuJIT AVX2
  Job-RIFECK : .NET 9.0.5 (9.0.525.21509), X64 RyuJIT AVX2

IterationCount=10

| Method                    | PkiOptions     | RevocationMode | Mean     | Error     | StdDev    |
|-------------------------- |--------------- |--------------- |---------:|----------:|----------:|
| DefaultHandshakePipeAsync | CrlEverywhere  | NoCheck        | 3.174 ms | 0.0361 ms | 0.0215 ms |
| DefaultHandshakePipeAsync | CrlEverywhere  | Online         | 3.125 ms | 0.0442 ms | 0.0231 ms |
| DefaultHandshakePipeAsync | OcspEverywhere | NoCheck        | 3.171 ms | 0.0649 ms | 0.0386 ms |
| DefaultHandshakePipeAsync | OcspEverywhere | Online         | 3.135 ms | 0.0347 ms | 0.0230 ms |
| DefaultHandshakePipeAsync | AllRevocation  | NoCheck        | 3.143 ms | 0.0631 ms | 0.0375 ms |
| DefaultHandshakePipeAsync | AllRevocation  | Online         | 3.178 ms | 0.0329 ms | 0.0196 ms |

I am not sure what causes the handshake to take longer in the CrlEverywhere+NoCheck option on windows, but overall it seems like there is no great perf impact on Windows an Linux (I can't check macos right now). probably because the CRL is cached so we still avoid outbound network request for each handshake.

rzikm added 2 commits June 16, 2025 08:27
…revocation-checking-by-default-for-SslStreamHttpClient
…revocation-checking-by-default-for-SslStreamHttpClient
@rzikm
Copy link
Member Author

rzikm commented Jun 16, 2025

/azp run runtime-libraries-coreclr outerloop

@rzikm
Copy link
Member Author

rzikm commented Jun 16, 2025

/azp run runtime-extra-platforms

Copy link

Azure Pipelines successfully started running 1 pipeline(s).

1 similar comment
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@rzikm
Copy link
Member Author

rzikm commented Jun 16, 2025

/ba-g Test failures are unrelated

@rzikm rzikm changed the title Change default CertificateRevocationMode to Online Change HttpClient/SslStream default certificate revocation check mode to Online Jun 16, 2025
@rzikm
Copy link
Member Author

rzikm commented Jun 16, 2025

Breaking change doc issue: dotnet/docs#46824

@rzikm rzikm merged commit 6a4b7e3 into dotnet:main Jun 16, 2025
120 of 144 checks passed
@rzikm rzikm removed the needs-breaking-change-doc-created Breaking changes need an issue opened with https://github.com/dotnet/docs/issues/new?template=dotnet label Jun 16, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-System.Net.Security breaking-change Issue or PR that represents a breaking API or functional change over a prerelease.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants