diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamCertificateContext.Linux.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamCertificateContext.Linux.cs index 0a5b5c89004b7..82ba564cc0a66 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamCertificateContext.Linux.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamCertificateContext.Linux.cs @@ -154,6 +154,17 @@ partial void AddRootCertificate(X509Certificate2? rootCertificate, ref bool tran return ValueTask.FromResult(_ocspResponse); } + internal ValueTask WaitForPendingOcspFetchAsync() + { + Task? pending = _pendingDownload; + if (pending is not null && !pending.IsFaulted) + { + return new ValueTask(pending); + } + + return ValueTask.FromResult(DateTimeOffset.UtcNow <= _ocspExpiration ? _ocspResponse : null); + } + private ValueTask DownloadOcspAsync() { Task? pending = _pendingDownload; diff --git a/src/libraries/System.Net.Security/tests/UnitTests/SslStreamCertificateContextOcspLinuxTests.cs b/src/libraries/System.Net.Security/tests/UnitTests/SslStreamCertificateContextOcspLinuxTests.cs index a4d705fa454d3..b712f814a02bb 100644 --- a/src/libraries/System.Net.Security/tests/UnitTests/SslStreamCertificateContextOcspLinuxTests.cs +++ b/src/libraries/System.Net.Security/tests/UnitTests/SslStreamCertificateContextOcspLinuxTests.cs @@ -15,6 +15,7 @@ namespace System.Net.Security.Tests; +[PlatformSpecific(TestPlatforms.Linux)] public class SslStreamCertificateContextOcspLinuxTests { [Fact] @@ -101,7 +102,8 @@ await SimpleTest(PkiOptions.OcspEverywhere, async (root, intermediate, endEntity intermediate.RevocationExpiration = DateTimeOffset.UtcNow.Add(SslStreamCertificateContext.MinRefreshBeforeExpirationInterval); SslStreamCertificateContext ctx = ctxFactory(false); - byte[] ocsp = await ctx.GetOcspResponseAsync(); + byte[] ocsp = await ctx.WaitForPendingOcspFetchAsync(); + Assert.NotNull(ocsp); intermediate.RevocationExpiration = DateTimeOffset.UtcNow.AddDays(1); @@ -111,12 +113,10 @@ await SimpleTest(PkiOptions.OcspEverywhere, async (root, intermediate, endEntity byte[] ocsp2 = ctx.GetOcspResponseNoWaiting(); Assert.Equal(ocsp, ocsp2); - await RetryHelper.ExecuteAsync(async () => - { - byte[] ocsp3 = await ctx.GetOcspResponseAsync(); - Assert.NotNull(ocsp3); - Assert.NotEqual(ocsp, ocsp3); - }, maxAttempts: 5, backoffFunc: i => (i + 1) * 200 /* ms */); + // The download should succeed + byte[] ocsp3 = await ctx.WaitForPendingOcspFetchAsync(); + Assert.NotNull(ocsp3); + Assert.NotEqual(ocsp, ocsp3); }); } @@ -128,7 +128,10 @@ await SimpleTest(PkiOptions.OcspEverywhere, async (root, intermediate, endEntity intermediate.RevocationExpiration = DateTimeOffset.UtcNow.AddSeconds(1); SslStreamCertificateContext ctx = ctxFactory(false); + // Make sure the inner OCSP fetch finished + await ctx.WaitForPendingOcspFetchAsync(); + // wait until the cached OCSP response expires await Task.Delay(2000); intermediate.RevocationExpiration = DateTimeOffset.UtcNow.AddDays(1); @@ -137,8 +140,8 @@ await SimpleTest(PkiOptions.OcspEverywhere, async (root, intermediate, endEntity byte[] ocsp = ctx.GetOcspResponseNoWaiting(); Assert.Null(ocsp); - // subsequent call will return the new response - byte[] ocsp2 = await ctx.GetOcspResponseAsync(); + // The download should succeed + byte[] ocsp2 = await ctx.WaitForPendingOcspFetchAsync(); Assert.NotNull(ocsp2); }); } @@ -155,25 +158,34 @@ await SimpleTest(PkiOptions.OcspEverywhere, async (root, intermediate, endEntity intermediate.RevocationExpiration = DateTimeOffset.UtcNow.Add(SslStreamCertificateContext.MinRefreshBeforeExpirationInterval); SslStreamCertificateContext ctx = ctxFactory(false); - byte[] ocsp = await ctx.GetOcspResponseAsync(); + // Make sure the inner OCSP fetch finished + byte[] ocsp = await ctx.WaitForPendingOcspFetchAsync(); Assert.NotNull(ocsp); responder.RespondKind = RespondKind.Invalid; - for (int i = 0; i < 3; i++) + for (int i = 0; i < 2; i++) { - await Task.Delay(SslStreamCertificateContext.RefreshAfterFailureBackOffInterval); byte[] ocsp2 = await ctx.GetOcspResponseAsync(); + await ctx.WaitForPendingOcspFetchAsync(); Assert.Equal(ocsp, ocsp2); + await Task.Delay(SslStreamCertificateContext.RefreshAfterFailureBackOffInterval.Add(TimeSpan.FromSeconds(1))); } + // make sure we try again only after backoff expires + await ctx.WaitForPendingOcspFetchAsync(); + await Task.Delay(SslStreamCertificateContext.RefreshAfterFailureBackOffInterval.Add(TimeSpan.FromSeconds(1))); + // after responder comes back online, the staple is eventually refreshed + intermediate.RevocationExpiration = DateTimeOffset.UtcNow.AddDays(1); responder.RespondKind = RespondKind.Normal; - await RetryHelper.ExecuteAsync(async () => - { - byte[] ocsp3 = await ctx.GetOcspResponseAsync(); - Assert.NotNull(ocsp3); - Assert.NotEqual(ocsp, ocsp3); - }, maxAttempts: 5, backoffFunc: i => (i + 1) * 200 /* ms */); + + // dispatch background refresh (first call still returns the old cached value) + await ctx.GetOcspResponseAsync(); + + // after refresh we should have a new staple + byte[] ocsp3 = await ctx.WaitForPendingOcspFetchAsync(); + Assert.NotNull(ocsp3); + Assert.NotEqual(ocsp, ocsp3); }); }