Skip to content

Commit 9e6f172

Browse files
authored
Fix disposing root X.509 certificate prematurely for OCSP stapling (#82116)
In SslStreamCertificateContext, don't dispose of the root cert if it's about to be handed to the AddRootCert PAL call, which was the high-level cause of a segfault when handling certificate chains of length 2 in OCSP Stapling on Linux. This change additionally guards against disposed certificates in the OCSP Stapling retriever (disabling the feature instead of segfaulting), and adds tests to ensure that we don't regress 2-cert chains in the future.
1 parent c108266 commit 9e6f172

File tree

4 files changed

+103
-65
lines changed

4 files changed

+103
-65
lines changed

src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/CertificateAuthority.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -854,8 +854,8 @@ internal static void BuildPrivatePki(
854854
rootAuthority = new CertificateAuthority(
855855
rootCert,
856856
rootDistributionViaHttp ? certUrl : null,
857-
issuerRevocationViaCrl ? cdpUrl : null,
858-
issuerRevocationViaOcsp ? ocspUrl : null);
857+
issuerRevocationViaCrl || (endEntityRevocationViaCrl && intermediateAuthorityCount == 0) ? cdpUrl : null,
858+
issuerRevocationViaOcsp || (endEntityRevocationViaOcsp && intermediateAuthorityCount == 0) ? ocspUrl : null);
859859

860860
CertificateAuthority issuingAuthority = rootAuthority;
861861
intermediateAuthorities = new CertificateAuthority[intermediateAuthorityCount];

src/libraries/System.Net.Security/src/System/Net/Security/SslStreamCertificateContext.Linux.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,12 @@ partial void SetNoOcspFetch(bool noOcspFetch)
7373
_staplingForbidden = noOcspFetch;
7474
}
7575

76-
partial void AddRootCertificate(X509Certificate2? rootCertificate)
76+
partial void AddRootCertificate(X509Certificate2? rootCertificate, ref bool transferredOwnership)
7777
{
7878
if (IntermediateCertificates.Length == 0)
7979
{
8080
_ca = rootCertificate;
81+
transferredOwnership = true;
8182
}
8283
else
8384
{
@@ -197,6 +198,17 @@ partial void AddRootCertificate(X509Certificate2? rootCertificate)
197198

198199
IntPtr subject = Certificate.Handle;
199200
IntPtr issuer = caCert.Handle;
201+
Debug.Assert(subject != 0);
202+
Debug.Assert(issuer != 0);
203+
204+
// This should not happen - but in the event that it does, we can't give null pointers when building the
205+
// request, so skip stapling, and set it as forbidden so we don't bother looking for new stapled responses
206+
// in the future.
207+
if (subject == 0 || issuer == 0)
208+
{
209+
_staplingForbidden = true;
210+
return null;
211+
}
200212

201213
using (SafeOcspRequestHandle ocspRequest = Interop.Crypto.X509BuildOcspRequest(subject, issuer))
202214
{

src/libraries/System.Net.Security/src/System/Net/Security/SslStreamCertificateContext.cs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,13 @@ internal static SslStreamCertificateContext Create(
9696
// Dispose the copy of the target cert.
9797
chain.ChainElements[0].Certificate.Dispose();
9898

99-
// Dispose the last cert, if we didn't include it.
100-
for (int i = count + 1; i < chain.ChainElements.Count; i++)
99+
// Dispose of the certificates that we do not need. If we are holding on to the root,
100+
// don't dispose of it.
101+
int stopDisposingChainPosition = root is null ?
102+
chain.ChainElements.Count :
103+
chain.ChainElements.Count - 1;
104+
105+
for (int i = count + 1; i < stopDisposingChainPosition; i++)
101106
{
102107
chain.ChainElements[i].Certificate.Dispose();
103108
}
@@ -109,12 +114,19 @@ internal static SslStreamCertificateContext Create(
109114
// On Linux, AddRootCertificate will start a background download of an OCSP response,
110115
// unless this context was built "offline", or this came from the internal Create(X509Certificate2)
111116
ctx.SetNoOcspFetch(offline || noOcspFetch);
112-
ctx.AddRootCertificate(root);
117+
118+
bool transferredOwnership = false;
119+
ctx.AddRootCertificate(root, ref transferredOwnership);
120+
121+
if (!transferredOwnership)
122+
{
123+
root?.Dispose();
124+
}
113125

114126
return ctx;
115127
}
116128

117-
partial void AddRootCertificate(X509Certificate2? rootCertificate);
129+
partial void AddRootCertificate(X509Certificate2? rootCertificate, ref bool transferredOwnership);
118130
partial void SetNoOcspFetch(bool noOcspFetch);
119131

120132
internal SslStreamCertificateContext Duplicate()

src/libraries/System.Net.Security/tests/FunctionalTests/CertificateValidationRemoteServer.cs

Lines changed: 72 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -106,17 +106,19 @@ public Task ConnectWithRevocation_WithCallback(bool checkRevocation)
106106
[PlatformSpecific(TestPlatforms.Linux)]
107107
[ConditionalTheory]
108108
[OuterLoop("Subject to system load race conditions")]
109-
[InlineData(false)]
110-
[InlineData(true)]
111-
public Task ConnectWithRevocation_StapledOcsp(bool offlineContext)
109+
[InlineData(false, false)]
110+
[InlineData(true, false)]
111+
[InlineData(false, true)]
112+
[InlineData(true, true)]
113+
public Task ConnectWithRevocation_StapledOcsp(bool offlineContext, bool noIntermediates)
112114
{
113115
// Offline will only work if
114116
// a) the revocation has been checked recently enough that it is cached, or
115117
// b) the server stapled the response
116118
//
117119
// At high load, the server's background fetch might not have completed before
118120
// this test runs.
119-
return ConnectWithRevocation_WithCallback_Core(X509RevocationMode.Offline, offlineContext);
121+
return ConnectWithRevocation_WithCallback_Core(X509RevocationMode.Offline, offlineContext, noIntermediates);
120122
}
121123

122124
[Fact]
@@ -192,7 +194,8 @@ static bool CertificateValidationCallback(
192194

193195
private async Task ConnectWithRevocation_WithCallback_Core(
194196
X509RevocationMode revocationMode,
195-
bool? offlineContext = false)
197+
bool? offlineContext = false,
198+
bool noIntermediates = false)
196199
{
197200
string offlinePart = offlineContext.HasValue ? offlineContext.GetValueOrDefault().ToString().ToLower() : "null";
198201
string serverName = $"{revocationMode.ToString().ToLower()}.{offlinePart}.server.example";
@@ -203,13 +206,15 @@ private async Task ConnectWithRevocation_WithCallback_Core(
203206
PkiOptions.EndEntityRevocationViaOcsp | PkiOptions.CrlEverywhere,
204207
out RevocationResponder responder,
205208
out CertificateAuthority rootAuthority,
206-
out CertificateAuthority intermediateAuthority,
209+
out CertificateAuthority[] intermediateAuthorities,
207210
out X509Certificate2 serverCert,
211+
intermediateAuthorityCount: noIntermediates ? 0 : 1,
208212
subjectName: serverName,
209213
keySize: 2048,
210214
extensions: TestHelper.BuildTlsServerCertExtensions(serverName));
211215

212-
X509Certificate2 issuerCert = intermediateAuthority.CloneIssuerCert();
216+
CertificateAuthority issuingAuthority = noIntermediates ? rootAuthority : intermediateAuthorities[0];
217+
X509Certificate2 issuerCert = issuingAuthority.CloneIssuerCert();
213218
X509Certificate2 rootCert = rootAuthority.CloneIssuerCert();
214219

215220
SslClientAuthenticationOptions clientOpts = new SslClientAuthenticationOptions
@@ -243,71 +248,80 @@ private async Task ConnectWithRevocation_WithCallback_Core(
243248
serverCert = temp;
244249
}
245250

246-
await using (clientStream)
247-
await using (serverStream)
248-
using (responder)
249-
using (rootAuthority)
250-
using (intermediateAuthority)
251-
using (serverCert)
252-
using (issuerCert)
253-
using (rootCert)
254-
await using (SslStream tlsClient = new SslStream(clientStream))
255-
await using (SslStream tlsServer = new SslStream(serverStream))
251+
try
256252
{
257-
intermediateAuthority.Revoke(serverCert, serverCert.NotBefore);
258-
259-
SslServerAuthenticationOptions serverOpts = new SslServerAuthenticationOptions();
260-
261-
if (offlineContext.HasValue)
253+
await using (clientStream)
254+
await using (serverStream)
255+
using (responder)
256+
using (rootAuthority)
257+
using (serverCert)
258+
using (issuerCert)
259+
using (rootCert)
260+
await using (SslStream tlsClient = new SslStream(clientStream))
261+
await using (SslStream tlsServer = new SslStream(serverStream))
262262
{
263-
serverOpts.ServerCertificateContext = SslStreamCertificateContext.Create(
264-
serverCert,
265-
new X509Certificate2Collection(issuerCert),
266-
offlineContext.GetValueOrDefault());
263+
issuingAuthority.Revoke(serverCert, serverCert.NotBefore);
267264

268-
if (revocationMode == X509RevocationMode.Offline)
265+
SslServerAuthenticationOptions serverOpts = new SslServerAuthenticationOptions();
266+
267+
if (offlineContext.HasValue)
269268
{
270-
if (offlineContext.GetValueOrDefault(false))
271-
{
272-
// Add a delay just to show we're not winning because of race conditions.
273-
await Task.Delay(200);
274-
}
275-
else
269+
serverOpts.ServerCertificateContext = SslStreamCertificateContext.Create(
270+
serverCert,
271+
new X509Certificate2Collection(issuerCert),
272+
offlineContext.GetValueOrDefault());
273+
274+
if (revocationMode == X509RevocationMode.Offline)
276275
{
277-
if (!OperatingSystem.IsLinux())
276+
if (offlineContext.GetValueOrDefault(false))
278277
{
279-
throw new InvalidOperationException(
280-
"This test configuration uses reflection and is only defined for Linux.");
278+
// Add a delay just to show we're not winning because of race conditions.
279+
await Task.Delay(200);
281280
}
282-
283-
FieldInfo pendingDownloadTaskField = typeof(SslStreamCertificateContext).GetField(
284-
"_pendingDownload",
285-
BindingFlags.Instance | BindingFlags.NonPublic);
286-
287-
if (pendingDownloadTaskField is null)
281+
else
288282
{
289-
throw new InvalidOperationException("Cannot find the pending download field.");
290-
}
291-
292-
Task download = (Task)pendingDownloadTaskField.GetValue(serverOpts.ServerCertificateContext);
293-
294-
// If it's null, it should mean it has already finished. If not, it might not have.
295-
if (download is not null)
296-
{
297-
await download;
283+
if (!OperatingSystem.IsLinux())
284+
{
285+
throw new InvalidOperationException(
286+
"This test configuration uses reflection and is only defined for Linux.");
287+
}
288+
289+
FieldInfo pendingDownloadTaskField = typeof(SslStreamCertificateContext).GetField(
290+
"_pendingDownload",
291+
BindingFlags.Instance | BindingFlags.NonPublic);
292+
293+
if (pendingDownloadTaskField is null)
294+
{
295+
throw new InvalidOperationException("Cannot find the pending download field.");
296+
}
297+
298+
Task download = (Task)pendingDownloadTaskField.GetValue(serverOpts.ServerCertificateContext);
299+
300+
// If it's null, it should mean it has already finished. If not, it might not have.
301+
if (download is not null)
302+
{
303+
await download;
304+
}
298305
}
299306
}
300307
}
308+
else
309+
{
310+
serverOpts.ServerCertificate = serverCert;
311+
}
312+
313+
Task serverTask = tlsServer.AuthenticateAsServerAsync(serverOpts);
314+
Task clientTask = tlsClient.AuthenticateAsClientAsync(clientOpts);
315+
316+
await TestConfiguration.WhenAllOrAnyFailedWithTimeout(clientTask, serverTask);
301317
}
302-
else
318+
}
319+
finally
320+
{
321+
foreach (CertificateAuthority intermediateAuthority in intermediateAuthorities)
303322
{
304-
serverOpts.ServerCertificate = serverCert;
323+
intermediateAuthority.Dispose();
305324
}
306-
307-
Task serverTask = tlsServer.AuthenticateAsServerAsync(serverOpts);
308-
Task clientTask = tlsClient.AuthenticateAsClientAsync(clientOpts);
309-
310-
await TestConfiguration.WhenAllOrAnyFailedWithTimeout(clientTask, serverTask);
311325
}
312326

313327
static bool CertificateValidationCallback(

0 commit comments

Comments
 (0)