-
Notifications
You must be signed in to change notification settings - Fork 5.1k
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
Change HttpClient/SslStream default certificate revocation check mode to Online #116098
Conversation
Added When you commit this breaking change:
Tagging @dotnet/compat for awareness of the breaking change. |
There was a problem hiding this 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 theOfflineRevocationByDefault
switch and apply it as the new default in client/server options. - Update unit and functional tests to explicitly set
CertificateRevocationCheckMode
toNoCheck
where needed and refactor certificate generation/cleanup to usePkiHolder
. - 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 |
src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs
Outdated
Show resolved
Hide resolved
src/libraries/Common/tests/System/Net/Configuration.Certificates.Dynamic.cs
Show resolved
Hide resolved
internal static readonly X509RevocationMode DefaultRevocationMode = | ||
AppContextSwitchHelper.GetBooleanConfig( | ||
"System.Net.Security.OfflineRevocationByDefault", | ||
"DOTNET_SYSTEM_NET_SECURITY_OFFLINEREVOCATIONBYDEFAULT") | ||
? X509RevocationMode.NoCheck : X509RevocationMode.Online; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would expect to see some RemoteExecutor tests confirming that these override modes work; like in https://github.com/dotnet/runtime/blob/main/src/libraries/System.Security.Cryptography/tests/X509Certificates/PfxIterationCountTests.CustomAppContextDataLimit.cs
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
Azure Pipelines successfully started running 1 pipeline(s). |
/azp run runtime-extra-platforms |
Azure Pipelines successfully started running 1 pipeline(s). |
/ba-g test failure (System.Runtime.Tests) is unrelated, other failures are known |
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
src/libraries/Common/tests/System/Net/Configuration.Certificates.Dynamic.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Net.Http.WinHttpHandler/src/System/Net/Http/WinHttpHandler.cs
Show resolved
Hide resolved
src/libraries/Common/tests/System/Net/Configuration.Certificates.Dynamic.cs
Outdated
Show resolved
Hide resolved
src/libraries/Common/tests/System/Net/Configuration.Certificates.Dynamic.cs
Outdated
Show resolved
Hide resolved
Benchmark results comparing handshake time with and without revocation checking (they can be easily run on .NET 9) Benchmark codeBenchmarkDotNet.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
}
}
}
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. |
…revocation-checking-by-default-for-SslStreamHttpClient
…revocation-checking-by-default-for-SslStreamHttpClient
/azp run runtime-libraries-coreclr outerloop |
/azp run runtime-extra-platforms |
Azure Pipelines successfully started running 1 pipeline(s). |
1 similar comment
Azure Pipelines successfully started running 1 pipeline(s). |
/ba-g Test failures are unrelated |
Breaking change doc issue: dotnet/docs#46824 |
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 trueDOTNET_SYSTEM_NET_SECURITY_NOREVOCATIONCHECKBYDEFAULT
environment variable to trueThis 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.