From a24db1ceb73e436eab32da43ae069832a04ce3dd Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Thu, 13 Aug 2020 13:30:42 -0400 Subject: [PATCH] Make Linux fetch timeout behavior for chain building more consistent with Windows Change the UrlRetrievalTimeout behavior on Linux to match Windows' existing behavior of using a per-operation timeout rather than cumulative. Windows 8.1 and Windows 10 seem to have different upper limits for the UrlRetrievalTimeout. Linux matches the Windows 10 version (which is lower: 1 minute). --- .../X509Certificates/RevocationResponder.cs | 30 ++ .../Pal.Unix/CertificateAssetDownloader.cs | 21 +- .../Cryptography/Pal.Unix/ChainPal.cs | 17 +- .../Cryptography/Pal.Unix/CrlCache.cs | 13 +- .../Pal.Unix/OpenSslX509ChainProcessor.cs | 24 +- .../tests/RevocationTests/TimeoutTests.cs | 300 ++++++++++++++++++ ...Cryptography.X509Certificates.Tests.csproj | 1 + 7 files changed, 368 insertions(+), 38 deletions(-) create mode 100644 src/libraries/System.Security.Cryptography.X509Certificates/tests/RevocationTests/TimeoutTests.cs diff --git a/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/RevocationResponder.cs b/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/RevocationResponder.cs index aa3df4cf8e27a..cd59fde644340 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/RevocationResponder.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/RevocationResponder.cs @@ -30,6 +30,9 @@ private readonly Dictionary _crlPaths public bool RespondEmpty { get; set; } + public TimeSpan ResponseDelay { get; set; } + public DelayedActionsFlag DelayedActions { get; set; } + private RevocationResponder(HttpListener listener, string uriPrefix) { _listener = listener; @@ -160,6 +163,12 @@ private void HandleRequest(HttpListenerContext context, ref bool responded) if (_aiaPaths.TryGetValue(url, out authority)) { + if (DelayedActions.HasFlag(DelayedActionsFlag.Aia)) + { + Trace($"Delaying response by {ResponseDelay}."); + Thread.Sleep(ResponseDelay); + } + byte[] certData = RespondEmpty ? Array.Empty() : authority.GetCertData(); responded = true; @@ -172,6 +181,12 @@ private void HandleRequest(HttpListenerContext context, ref bool responded) if (_crlPaths.TryGetValue(url, out authority)) { + if (DelayedActions.HasFlag(DelayedActionsFlag.Crl)) + { + Trace($"Delaying response by {ResponseDelay}."); + Thread.Sleep(ResponseDelay); + } + byte[] crl = RespondEmpty ? Array.Empty() : authority.GetCrl(); responded = true; @@ -211,6 +226,12 @@ private void HandleRequest(HttpListenerContext context, ref bool responded) byte[] ocspResponse = RespondEmpty ? Array.Empty() : authority.BuildOcspResponse(certId, nonce); + if (DelayedActions.HasFlag(DelayedActionsFlag.Ocsp)) + { + Trace($"Delaying response by {ResponseDelay}."); + Thread.Sleep(ResponseDelay); + } + responded = true; context.Response.StatusCode = 200; context.Response.StatusDescription = "OK"; @@ -352,5 +373,14 @@ private static void Trace(string trace) Console.WriteLine(trace); } } + + internal enum DelayedActionsFlag : byte + { + None = 0, + Ocsp = 0b1, + Crl = 0b10, + Aia = 0b100, + All = 0b11111111 + } } } diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/CertificateAssetDownloader.cs b/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/CertificateAssetDownloader.cs index 7b0b166c8d12b..c6b9fef9e115b 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/CertificateAssetDownloader.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/CertificateAssetDownloader.cs @@ -18,9 +18,9 @@ internal static class CertificateAssetDownloader { private static readonly Func? s_downloadBytes = CreateDownloadBytesFunc(); - internal static X509Certificate2? DownloadCertificate(string uri, ref TimeSpan remainingDownloadTime) + internal static X509Certificate2? DownloadCertificate(string uri, TimeSpan downloadTimeout) { - byte[]? data = DownloadAsset(uri, ref remainingDownloadTime); + byte[]? data = DownloadAsset(uri, downloadTimeout); if (data == null || data.Length == 0) { @@ -39,9 +39,9 @@ internal static class CertificateAssetDownloader } } - internal static SafeX509CrlHandle? DownloadCrl(string uri, ref TimeSpan remainingDownloadTime) + internal static SafeX509CrlHandle? DownloadCrl(string uri, TimeSpan downloadTimeout) { - byte[]? data = DownloadAsset(uri, ref remainingDownloadTime); + byte[]? data = DownloadAsset(uri, downloadTimeout); if (data == null) { @@ -77,9 +77,9 @@ internal static class CertificateAssetDownloader return null; } - internal static SafeOcspResponseHandle? DownloadOcspGet(string uri, ref TimeSpan remainingDownloadTime) + internal static SafeOcspResponseHandle? DownloadOcspGet(string uri, TimeSpan downloadTimeout) { - byte[]? data = DownloadAsset(uri, ref remainingDownloadTime); + byte[]? data = DownloadAsset(uri, downloadTimeout); if (data == null) { @@ -100,12 +100,11 @@ internal static class CertificateAssetDownloader return resp; } - private static byte[]? DownloadAsset(string uri, ref TimeSpan remainingDownloadTime) + private static byte[]? DownloadAsset(string uri, TimeSpan downloadTimeout) { - if (s_downloadBytes != null && remainingDownloadTime > TimeSpan.Zero) + if (s_downloadBytes != null && downloadTimeout > TimeSpan.Zero) { - long totalMillis = (long)remainingDownloadTime.TotalMilliseconds; - Stopwatch stopwatch = Stopwatch.StartNew(); + long totalMillis = (long)downloadTimeout.TotalMilliseconds; CancellationTokenSource? cts = totalMillis > int.MaxValue ? null : new CancellationTokenSource((int)totalMillis); try @@ -115,8 +114,6 @@ internal static class CertificateAssetDownloader catch { } finally { - // TimeSpan.Zero isn't a worrisome value on the subtraction, it only means "no limit" on the original input. - remainingDownloadTime -= stopwatch.Elapsed; cts?.Dispose(); } } diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/ChainPal.cs b/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/ChainPal.cs index 5e37365827e6b..d32d8b58c837d 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/ChainPal.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/ChainPal.cs @@ -11,6 +11,8 @@ namespace Internal.Cryptography.Pal { internal sealed partial class ChainPal { + private static readonly TimeSpan s_maxUrlRetrievalTimeout = TimeSpan.FromMinutes(1); + public static IChainPal FromHandle(IntPtr chainContext) { throw new PlatformNotSupportedException(); @@ -35,10 +37,17 @@ public static IChainPal BuildChain( TimeSpan timeout, bool disableAia) { - // An input value of 0 on the timeout is "take all the time you need". if (timeout == TimeSpan.Zero) { - timeout = TimeSpan.MaxValue; + // An input value of 0 on the timeout is treated as 15 seconds, to match Windows. + timeout = TimeSpan.FromSeconds(15); + } + else if (timeout > s_maxUrlRetrievalTimeout || timeout < TimeSpan.Zero) + { + // Windows has a max timeout of 1 minute, so we'll match. Windows also treats + // the timeout as unsigned, so a negative value gets treated as a large positive + // value that is also clamped. + timeout = s_maxUrlRetrievalTimeout; } // Let Unspecified mean Local, so only convert if the source was UTC. @@ -55,14 +64,14 @@ public static IChainPal BuildChain( { } - TimeSpan remainingDownloadTime = timeout; + TimeSpan downloadTimeout = timeout; OpenSslX509ChainProcessor chainPal = OpenSslX509ChainProcessor.InitiateChain( ((OpenSslX509CertificateReader)cert).SafeHandle, customTrustStore, trustMode, verificationTime, - remainingDownloadTime); + downloadTimeout); Interop.Crypto.X509VerifyStatusCode status = chainPal.FindFirstChain(extraStore); diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/CrlCache.cs b/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/CrlCache.cs index 16e2dc5e26008..d1c6aa7cbe444 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/CrlCache.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/CrlCache.cs @@ -36,7 +36,7 @@ public static void AddCrlForCertificate( SafeX509StoreHandle store, X509RevocationMode revocationMode, DateTime verificationTime, - ref TimeSpan remainingDownloadTime) + TimeSpan downloadTimeout) { // In Offline mode, accept any cached CRL we have. // "CRL is Expired" is a better match for Offline than "Could not find CRL" @@ -59,14 +59,13 @@ public static void AddCrlForCertificate( return; } - // Don't do any work if we're over limit or prohibited from fetching new CRLs - if (remainingDownloadTime <= TimeSpan.Zero || - revocationMode != X509RevocationMode.Online) + // Don't do any work if we're prohibited from fetching new CRLs + if (revocationMode != X509RevocationMode.Online) { return; } - DownloadAndAddCrl(url, crlFileName, store, ref remainingDownloadTime); + DownloadAndAddCrl(url, crlFileName, store, downloadTimeout); } private static bool AddCachedCrl(string crlFileName, SafeX509StoreHandle store, DateTime verificationTime) @@ -156,11 +155,11 @@ private static void DownloadAndAddCrl( string url, string crlFileName, SafeX509StoreHandle store, - ref TimeSpan remainingDownloadTime) + TimeSpan downloadTimeout) { // X509_STORE_add_crl will increase the refcount on the CRL object, so we should still // dispose our copy. - using (SafeX509CrlHandle? crl = CertificateAssetDownloader.DownloadCrl(url, ref remainingDownloadTime)) + using (SafeX509CrlHandle? crl = CertificateAssetDownloader.DownloadCrl(url, downloadTimeout)) { // null is a valid return (e.g. no remainingDownloadTime) if (crl != null && !crl.IsInvalid) diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/OpenSslX509ChainProcessor.cs b/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/OpenSslX509ChainProcessor.cs index a41997057ee7b..8fd228dfe9ebd 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/OpenSslX509ChainProcessor.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/OpenSslX509ChainProcessor.cs @@ -43,7 +43,7 @@ internal sealed class OpenSslX509ChainProcessor : IChainPal private readonly SafeX509StackHandle _untrustedLookup; private readonly SafeX509StoreCtxHandle _storeCtx; private readonly DateTime _verificationTime; - private TimeSpan _remainingDownloadTime; + private readonly TimeSpan _downloadTimeout; private WorkingChain? _workingChain; private OpenSslX509ChainProcessor( @@ -52,14 +52,14 @@ private OpenSslX509ChainProcessor( SafeX509StackHandle untrusted, SafeX509StoreCtxHandle storeCtx, DateTime verificationTime, - TimeSpan remainingDownloadTime) + TimeSpan downloadTimeout) { _leafHandle = leafHandle; _store = store; _untrustedLookup = untrusted; _storeCtx = storeCtx; _verificationTime = verificationTime; - _remainingDownloadTime = remainingDownloadTime; + _downloadTimeout = downloadTimeout; } public void Dispose() @@ -236,7 +236,7 @@ internal Interop.Crypto.X509VerifyStatusCode FindChainViaAia( X509Certificate2? downloaded = DownloadCertificate( authorityInformationAccess, - ref _remainingDownloadTime); + _downloadTimeout); // The AIA record is contained in a public structure, so no need to clear it. CryptoPool.Return(authorityInformationAccess.Array!, clearSize: 0); @@ -366,7 +366,7 @@ internal void ProcessRevocation( _store, revocationMode, _verificationTime, - ref _remainingDownloadTime); + _downloadTimeout); } } } @@ -655,7 +655,7 @@ private Interop.Crypto.X509VerifyStatusCode CheckOcsp( return status; } - if (revocationMode != X509RevocationMode.Online || _remainingDownloadTime <= TimeSpan.Zero) + if (revocationMode != X509RevocationMode.Online) { return Interop.Crypto.X509VerifyStatusCode.X509_V_ERR_UNABLE_TO_GET_CRL; } @@ -690,7 +690,7 @@ private Interop.Crypto.X509VerifyStatusCode CheckOcsp( // // So, for now, only try GET. SafeOcspResponseHandle? resp = - CertificateAssetDownloader.DownloadOcspGet(requestUrl, ref _remainingDownloadTime); + CertificateAssetDownloader.DownloadOcspGet(requestUrl, _downloadTimeout); using (resp) { @@ -1072,14 +1072,8 @@ private static X509ChainStatusFlags MapVerifyErrorToChainStatus(Interop.Crypto.X private static X509Certificate2? DownloadCertificate( ReadOnlyMemory authorityInformationAccess, - ref TimeSpan remainingDownloadTime) + TimeSpan downloadTimeout) { - // Don't do any work if we're over limit. - if (remainingDownloadTime <= TimeSpan.Zero) - { - return null; - } - string? uri = FindHttpAiaRecord(authorityInformationAccess, Oids.CertificateAuthorityIssuers); if (uri == null) @@ -1087,7 +1081,7 @@ private static X509ChainStatusFlags MapVerifyErrorToChainStatus(Interop.Crypto.X return null; } - return CertificateAssetDownloader.DownloadCertificate(uri, ref remainingDownloadTime); + return CertificateAssetDownloader.DownloadCertificate(uri, downloadTimeout); } private static string? GetOcspEndpoint(SafeX509Handle cert) diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/RevocationTests/TimeoutTests.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/RevocationTests/TimeoutTests.cs new file mode 100644 index 0000000000000..57deca86d9790 --- /dev/null +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/RevocationTests/TimeoutTests.cs @@ -0,0 +1,300 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Security.Cryptography.X509Certificates.Tests.Common; +using System.Linq; +using Xunit; + +namespace System.Security.Cryptography.X509Certificates.Tests.RevocationTests +{ + [OuterLoop("These tests exercise timeout properties which take a lot of time.")] + public static class TimeoutTests + { + [Theory] + [InlineData(PkiOptions.OcspEverywhere)] + [InlineData(PkiOptions.CrlEverywhere)] + [PlatformSpecific(TestPlatforms.Windows | TestPlatforms.Linux)] + public static void RevocationCheckingDelayed(PkiOptions pkiOptions) + { + CertificateAuthority.BuildPrivatePki( + pkiOptions, + out RevocationResponder responder, + out CertificateAuthority rootAuthority, + out CertificateAuthority intermediateAuthority, + out X509Certificate2 endEntityCert, + nameof(RevocationCheckingDelayed)); + + using (responder) + using (rootAuthority) + using (intermediateAuthority) + using (endEntityCert) + using (ChainHolder holder = new ChainHolder()) + using (X509Certificate2 rootCert = rootAuthority.CloneIssuerCert()) + using (X509Certificate2 intermediateCert = intermediateAuthority.CloneIssuerCert()) + { + TimeSpan delay = TimeSpan.FromSeconds(3); + + X509Chain chain = holder.Chain; + responder.ResponseDelay = delay; + responder.DelayedActions = RevocationResponder.DelayedActionsFlag.All; + + // This needs to be greater than delay, but less than 2x delay to ensure + // that the time is a timeout for individual fetches, not a running total. + chain.ChainPolicy.UrlRetrievalTimeout = TimeSpan.FromSeconds(5); + chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; + chain.ChainPolicy.CustomTrustStore.Add(rootCert); + chain.ChainPolicy.ExtraStore.Add(intermediateCert); + chain.ChainPolicy.RevocationMode = X509RevocationMode.Online; + chain.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot; + + chain.ChainPolicy.DisableCertificateDownloads = true; + + Stopwatch watch = Stopwatch.StartNew(); + Assert.True(chain.Build(endEntityCert)); + watch.Stop(); + + // There should be two network fetches, OCSP/CRL to intermediate to get leaf status, + // OCSP/CRL to root to get intermediate statuses. It should take at least 2x the delay + // plus other non-network time, so we can at least ensure it took as long as + // the delay for each fetch. + Assert.True(watch.Elapsed >= delay * 2); + } + } + + [Theory] + [InlineData(PkiOptions.OcspEverywhere)] + [InlineData(PkiOptions.CrlEverywhere)] + [PlatformSpecific(TestPlatforms.Windows | TestPlatforms.Linux)] + public static void RevocationCheckingTimeout(PkiOptions pkiOptions) + { + CertificateAuthority.BuildPrivatePki( + pkiOptions, + out RevocationResponder responder, + out CertificateAuthority rootAuthority, + out CertificateAuthority intermediateAuthority, + out X509Certificate2 endEntityCert, + nameof(RevocationCheckingTimeout)); + + using (responder) + using (rootAuthority) + using (intermediateAuthority) + using (endEntityCert) + using (ChainHolder holder = new ChainHolder()) + using (X509Certificate2 rootCert = rootAuthority.CloneIssuerCert()) + using (X509Certificate2 intermediateCert = intermediateAuthority.CloneIssuerCert()) + { + TimeSpan delay = TimeSpan.FromSeconds(3); + + X509Chain chain = holder.Chain; + responder.ResponseDelay = delay; + responder.DelayedActions = RevocationResponder.DelayedActionsFlag.All; + + chain.ChainPolicy.UrlRetrievalTimeout = TimeSpan.FromSeconds(1); + chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; + chain.ChainPolicy.CustomTrustStore.Add(rootCert); + chain.ChainPolicy.ExtraStore.Add(intermediateCert); + chain.ChainPolicy.RevocationMode = X509RevocationMode.Online; + chain.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot; + + chain.ChainPolicy.DisableCertificateDownloads = true; + + Assert.False(chain.Build(endEntityCert)); + + const X509ChainStatusFlags ExpectedFlags = + X509ChainStatusFlags.RevocationStatusUnknown | + X509ChainStatusFlags.OfflineRevocation; + + X509ChainStatusFlags eeFlags = GetFlags(chain, endEntityCert.Thumbprint); + X509ChainStatusFlags intermediateFlags = GetFlags(chain, intermediateCert.Thumbprint); + Assert.Equal(ExpectedFlags, eeFlags); + Assert.Equal(ExpectedFlags, intermediateFlags); + } + } + + [Theory] + [InlineData(PkiOptions.OcspEverywhere)] + [InlineData(PkiOptions.CrlEverywhere)] + [PlatformSpecific(TestPlatforms.Windows | TestPlatforms.Linux)] + public static void RevocationCheckingMaximum(PkiOptions pkiOptions) + { + // Windows 10 has a different maximum from previous versions of Windows. + // We are primarily testing that Linux behavior matches some behavior of + // Windows, so we won't test except on Windows 10. + if (PlatformDetection.WindowsVersion < 10) + { + return; + } + + CertificateAuthority.BuildPrivatePki( + pkiOptions, + out RevocationResponder responder, + out CertificateAuthority rootAuthority, + out CertificateAuthority intermediateAuthority, + out X509Certificate2 endEntityCert, + nameof(RevocationCheckingMaximum)); + + using (responder) + using (rootAuthority) + using (intermediateAuthority) + using (endEntityCert) + using (ChainHolder holder = new ChainHolder()) + using (X509Certificate2 rootCert = rootAuthority.CloneIssuerCert()) + using (X509Certificate2 intermediateCert = intermediateAuthority.CloneIssuerCert()) + { + TimeSpan delay = TimeSpan.FromMinutes(1.5); + + X509Chain chain = holder.Chain; + responder.ResponseDelay = delay; + responder.DelayedActions = RevocationResponder.DelayedActionsFlag.All; + + chain.ChainPolicy.UrlRetrievalTimeout = TimeSpan.FromMinutes(2); + chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; + chain.ChainPolicy.CustomTrustStore.Add(rootCert); + chain.ChainPolicy.ExtraStore.Add(intermediateCert); + chain.ChainPolicy.RevocationMode = X509RevocationMode.Online; + chain.ChainPolicy.RevocationFlag = X509RevocationFlag.EndCertificateOnly; + + chain.ChainPolicy.DisableCertificateDownloads = true; + + // Even though UrlRetrievalTimeout is more than the delay, it should + // get clamped to 1 minute, and thus less than the actual delay. + Assert.False(chain.Build(endEntityCert)); + + const X509ChainStatusFlags ExpectedFlags = + X509ChainStatusFlags.RevocationStatusUnknown | + X509ChainStatusFlags.OfflineRevocation; + + X509ChainStatusFlags eeFlags = GetFlags(chain, endEntityCert.Thumbprint); + Assert.Equal(ExpectedFlags, eeFlags); + } + } + + [Theory] + [InlineData(PkiOptions.OcspEverywhere)] + [InlineData(PkiOptions.CrlEverywhere)] + [PlatformSpecific(TestPlatforms.Windows | TestPlatforms.Linux)] + public static void RevocationCheckingNegativeTimeout(PkiOptions pkiOptions) + { + CertificateAuthority.BuildPrivatePki( + pkiOptions, + out RevocationResponder responder, + out CertificateAuthority rootAuthority, + out CertificateAuthority intermediateAuthority, + out X509Certificate2 endEntityCert, + nameof(RevocationCheckingNegativeTimeout)); + + using (responder) + using (rootAuthority) + using (intermediateAuthority) + using (endEntityCert) + using (ChainHolder holder = new ChainHolder()) + using (X509Certificate2 rootCert = rootAuthority.CloneIssuerCert()) + using (X509Certificate2 intermediateCert = intermediateAuthority.CloneIssuerCert()) + { + // Delay is more than the 15 second default. + TimeSpan delay = TimeSpan.FromSeconds(25); + + X509Chain chain = holder.Chain; + responder.ResponseDelay = delay; + responder.DelayedActions = RevocationResponder.DelayedActionsFlag.All; + + chain.ChainPolicy.UrlRetrievalTimeout = TimeSpan.FromMinutes(-1); + chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; + chain.ChainPolicy.CustomTrustStore.Add(rootCert); + chain.ChainPolicy.ExtraStore.Add(intermediateCert); + chain.ChainPolicy.RevocationMode = X509RevocationMode.Online; + chain.ChainPolicy.RevocationFlag = X509RevocationFlag.EndCertificateOnly; + + chain.ChainPolicy.DisableCertificateDownloads = true; + + Assert.True(chain.Build(endEntityCert)); + } + } + + [Fact] + [PlatformSpecific(TestPlatforms.Linux)] + public static void AiaFetchDelayed() + { + CertificateAuthority.BuildPrivatePki( + PkiOptions.OcspEverywhere, + out RevocationResponder responder, + out CertificateAuthority rootAuthority, + out CertificateAuthority intermediateAuthority, + out X509Certificate2 endEntityCert, + nameof(AiaFetchDelayed)); + + using (responder) + using (rootAuthority) + using (intermediateAuthority) + using (endEntityCert) + using (ChainHolder holder = new ChainHolder()) + using (X509Certificate2 rootCert = rootAuthority.CloneIssuerCert()) + using (X509Certificate2 intermediateCert = intermediateAuthority.CloneIssuerCert()) + { + TimeSpan delay = TimeSpan.FromSeconds(1); + + X509Chain chain = holder.Chain; + responder.ResponseDelay = delay; + responder.DelayedActions = RevocationResponder.DelayedActionsFlag.All; + + chain.ChainPolicy.UrlRetrievalTimeout = TimeSpan.FromSeconds(5); + chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; + chain.ChainPolicy.CustomTrustStore.Add(rootCert); + chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; + + Stopwatch watch = Stopwatch.StartNew(); + Assert.True(chain.Build(endEntityCert), GetFlags(chain, endEntityCert.Thumbprint).ToString()); + watch.Stop(); + + Assert.True(watch.Elapsed >= delay); + } + } + + [Fact] + [PlatformSpecific(TestPlatforms.Linux)] + public static void AiaFetchTimeout() + { + CertificateAuthority.BuildPrivatePki( + PkiOptions.AllRevocation, + out RevocationResponder responder, + out CertificateAuthority rootAuthority, + out CertificateAuthority intermediateAuthority, + out X509Certificate2 endEntityCert, + nameof(AiaFetchTimeout)); + + using (responder) + using (rootAuthority) + using (intermediateAuthority) + using (endEntityCert) + using (ChainHolder holder = new ChainHolder()) + using (X509Certificate2 rootCert = rootAuthority.CloneIssuerCert()) + using (X509Certificate2 intermediateCert = intermediateAuthority.CloneIssuerCert()) + { + TimeSpan delay = TimeSpan.FromSeconds(3); + + X509Chain chain = holder.Chain; + responder.ResponseDelay = delay; + responder.DelayedActions = RevocationResponder.DelayedActionsFlag.All; + + chain.ChainPolicy.UrlRetrievalTimeout = TimeSpan.FromSeconds(2); + chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; + chain.ChainPolicy.CustomTrustStore.Add(rootCert); + chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; + + Assert.False(chain.Build(endEntityCert)); + + const X509ChainStatusFlags ExpectedFlags = + X509ChainStatusFlags.PartialChain; + + X509ChainStatusFlags eeFlags = GetFlags(chain, endEntityCert.Thumbprint); + Assert.Equal(ExpectedFlags, eeFlags); + } + } + + private static X509ChainStatusFlags GetFlags(X509Chain chain, string thumbprint) => + chain.ChainElements.OfType(). + Single(e => e.Certificate.Thumbprint == thumbprint). + ChainElementStatus.Aggregate((X509ChainStatusFlags)0, (a, e) => a | e.Status); + } +} diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/System.Security.Cryptography.X509Certificates.Tests.csproj b/src/libraries/System.Security.Cryptography.X509Certificates/tests/System.Security.Cryptography.X509Certificates.Tests.csproj index c666f2f3e54cb..7d818904df241 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/tests/System.Security.Cryptography.X509Certificates.Tests.csproj +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/System.Security.Cryptography.X509Certificates.Tests.csproj @@ -58,6 +58,7 @@ +