From 11b7dfa55d53dffca06a35c57346c8e86815b5d4 Mon Sep 17 00:00:00 2001 From: Koen van Leeuwen Date: Fri, 26 Jun 2026 09:15:04 +0200 Subject: [PATCH 1/5] Add managed regression tests for timezone-dependent validity comparisons FindByTimeValid and CertificateRequest.Create's issuer-nesting check compare certificate validity against local-kind DateTime, so a mid-process timezone change can flip the result even though the underlying certificate validity instant hasn't moved. These tests fail today; the following commits fix the underlying comparisons. --- .../ChainTests.TimeZone.Linux.cs | 86 ++++++++++++++++++- 1 file changed, 83 insertions(+), 3 deletions(-) diff --git a/src/libraries/System.Security.Cryptography/tests/X509Certificates/ChainTests.TimeZone.Linux.cs b/src/libraries/System.Security.Cryptography/tests/X509Certificates/ChainTests.TimeZone.Linux.cs index eb4f636fcc0b0d..0e4fea41d969af 100644 --- a/src/libraries/System.Security.Cryptography/tests/X509Certificates/ChainTests.TimeZone.Linux.cs +++ b/src/libraries/System.Security.Cryptography/tests/X509Certificates/ChainTests.TimeZone.Linux.cs @@ -6,7 +6,7 @@ namespace System.Security.Cryptography.X509Certificates.Tests { - // Chain time validity must not depend on the process time zone. + // Chain and managed certificate time validity must not depend on the process time zone. // OpenSSL/Linux only (Windows/macOS convert the verify time correctly). // RemoteExecutor + TZ isolates the time-zone change to a child process. [PlatformSpecific(TestPlatforms.Linux)] @@ -14,6 +14,11 @@ public static class ChainTimeZoneTests { private static readonly DateTimeOffset s_verify = new(2024, 6, 15, 12, 0, 0, TimeSpan.Zero); + // Validity window of +/-2h around the query, narrower than the UTC+14 shift below. + private static readonly DateTimeOffset s_managedNotBefore = new(2024, 6, 15, 10, 0, 0, TimeSpan.Zero); + private static readonly DateTimeOffset s_managedNotAfter = new(2024, 6, 15, 14, 0, 0, TimeSpan.Zero); + private static readonly DateTimeOffset s_managedQuery = new(2024, 6, 15, 13, 0, 0, TimeSpan.Zero); + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] [InlineData(false)] [InlineData(true)] @@ -55,16 +60,91 @@ public static void ExpiredCert_StaysNotTimeValidAfterTimeZoneChange(bool asLocal }, asLocal.ToString()).Dispose(); } + // Find(FindByTimeValid) for an instant inside the validity window must keep matching + // across a mid-process time-zone change. + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public static void FindByTimeValid_StableAcrossTimeZoneChange() + { + RemoteExecutor.Invoke(static () => + { + SetZone("UTC"); + + using X509Certificate2 cert = MakeCert(s_managedNotBefore, s_managedNotAfter, "CN=managed-tz"); + + // Read the validity dates before the zone change. + _ = cert.NotBefore; + _ = cert.NotAfter; + + var coll = new X509Certificate2Collection(cert); + Assert.Equal(1, FindCount(coll, s_managedQuery)); + + SetZone("Pacific/Kiritimati"); // UTC+14 + Assert.Equal(1, FindCount(coll, s_managedQuery)); + }).Dispose(); + } + + // CertificateRequest.Create checks that the leaf validity nests inside the issuer's. + // A request that nests must keep succeeding across a mid-process time-zone change. + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public static void CreateNesting_StableAcrossTimeZoneChange() + { + RemoteExecutor.Invoke(static () => + { + SetZone("UTC"); + + DateTimeOffset issuerNotBefore = new(2024, 6, 15, 0, 0, 0, TimeSpan.Zero); + DateTimeOffset issuerNotAfter = new(2025, 6, 15, 0, 0, 0, TimeSpan.Zero); + + using RSA issuerKey = RSA.Create(2048); + var issuerReq = new CertificateRequest("CN=issuer", issuerKey, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + issuerReq.CertificateExtensions.Add(new X509BasicConstraintsExtension(true, false, 0, true)); + using X509Certificate2 issuer = issuerReq.CreateSelfSigned(issuerNotBefore, issuerNotAfter); + + // Leaf nests 1h inside the issuer on each boundary. + DateTimeOffset leafNotBefore = issuerNotBefore.AddHours(1); + DateTimeOffset leafNotAfter = issuerNotAfter.AddHours(-1); + + using RSA leafKey = RSA.Create(2048); + var leafReq = new CertificateRequest("CN=leaf", leafKey, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + byte[] serial = [1, 2, 3, 4, 5, 6, 7, 8]; + + // Read the issuer's validity dates before the zone change. + _ = issuer.NotBefore; + _ = issuer.NotAfter; + + using (X509Certificate2 leaf = leafReq.Create(issuer, leafNotBefore, leafNotAfter, serial)) + { + Assert.NotNull(leaf); + } + + SetZone("Pacific/Kiritimati"); // UTC+14 + + using X509Certificate2 leaf2 = leafReq.Create(issuer, leafNotBefore, leafNotAfter, serial); + Assert.NotNull(leaf2); + }).Dispose(); + } + + private static int FindCount(X509Certificate2Collection coll, DateTimeOffset when) + { + X509Certificate2Collection found = coll.Find(X509FindType.FindByTimeValid, when.UtcDateTime, validOnly: false); + int count = found.Count; + foreach (X509Certificate2 c in found) + { + c.Dispose(); + } + return count; + } + internal static void SetZone(string tz) { Environment.SetEnvironmentVariable("TZ", tz); TimeZoneInfo.ClearCachedData(); } - internal static X509Certificate2 MakeCert(DateTimeOffset notBefore, DateTimeOffset notAfter) + internal static X509Certificate2 MakeCert(DateTimeOffset notBefore, DateTimeOffset notAfter, string subjectName = "CN=109039") { using RSA key = RSA.Create(2048); - var req = new CertificateRequest("CN=109039", key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + var req = new CertificateRequest(subjectName, key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); return req.CreateSelfSigned(notBefore, notAfter); } From 67124d5866c165c709487bf0256760db2ca8ff4b Mon Sep 17 00:00:00 2001 From: Koen van Leeuwen Date: Fri, 26 Jun 2026 09:18:06 +0200 Subject: [PATCH 2/5] Change ICertificatePalCore.NotBefore/NotAfter to UTC DateTimeOffset X.509 validity periods are absolute instants encoded in UTC. The PAL layer was decoding that UTC instant correctly and then immediately converting it to a local-kind DateTime, forcing every internal consumer to convert back to compare correctly. Converting to local time and back is exactly the antipattern behind #109039: values that are fundamentally UTC instants get round-tripped through local time, so results depend on the machine's time zone and DST. Have every PAL backend (Windows, Apple, Android, OpenSSL) expose the UTC DateTimeOffset it already computes, and update internal consumers (X509Certificate's lazy cache, CertificateRequest's issuer-nesting check, ManagedCertificateFinder's time-based Find, the Apple chain processor's verification-time field, the OpenSSL CRL cache) to compare in UTC instead of local time. The public X509Certificate2.NotBefore/ NotAfter properties are unchanged (DateTimeKind.Local), projected from the cached UTC instant at the single public boundary. This turns the regression tests added in the previous commit green. --- .../Windows/Crypt32/Interop.CERT_INFO.cs | 6 +++ .../X509Certificates/AndroidCertificatePal.cs | 8 ++-- .../AppleCertificatePal.TempExportPal.cs | 4 +- .../X509Certificates/AppleCertificatePal.cs | 8 ++-- .../CertificateData.ManagedDecode.cs | 4 +- .../CertificatePal.Windows.cs | 8 ++-- .../X509Certificates/CertificateRequest.cs | 19 ++++------ .../X509Certificates/ChainPal.Apple.cs | 2 +- .../X509Certificates/ICertificatePalCore.cs | 4 +- .../ManagedCertificateFinder.cs | 25 +++++------- .../X509Certificates/OpenSslCrlCache.cs | 38 ++++++++----------- .../OpenSslX509CertificateReader.cs | 10 +++-- .../OpenSslX509ChainProcessor.cs | 2 +- .../X509Certificates/X509Certificate.cs | 28 +++++++------- .../X509Certificates/X509Certificate2.cs | 4 +- 15 files changed, 81 insertions(+), 89 deletions(-) diff --git a/src/libraries/Common/src/Interop/Windows/Crypt32/Interop.CERT_INFO.cs b/src/libraries/Common/src/Interop/Windows/Crypt32/Interop.CERT_INFO.cs index b7d20b38ce683f..29a4009741d9b3 100644 --- a/src/libraries/Common/src/Interop/Windows/Crypt32/Interop.CERT_INFO.cs +++ b/src/libraries/Common/src/Interop/Windows/Crypt32/Interop.CERT_INFO.cs @@ -37,6 +37,12 @@ internal DateTime ToDateTime() return DateTime.FromFileTime(fileTime); } + internal DateTimeOffset ToDateTimeOffset() + { + long fileTime = (((long)ftTimeHigh) << 32) + ftTimeLow; + return new DateTimeOffset(DateTime.FromFileTimeUtc(fileTime)); + } + internal static FILETIME FromDateTime(DateTime dt) { long fileTime = dt.ToFileTime(); diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AndroidCertificatePal.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AndroidCertificatePal.cs index dd208147371fa5..ae4e2709264e67 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AndroidCertificatePal.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AndroidCertificatePal.cs @@ -280,21 +280,21 @@ public string SignatureAlgorithm } } - public DateTime NotAfter + public DateTimeOffset NotAfter { get { EnsureCertificateData(); - return _certData.NotAfter.ToLocalTime(); + return _certData.NotAfter; } } - public DateTime NotBefore + public DateTimeOffset NotBefore { get { EnsureCertificateData(); - return _certData.NotBefore.ToLocalTime(); + return _certData.NotBefore; } } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AppleCertificatePal.TempExportPal.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AppleCertificatePal.TempExportPal.cs index dbe1bc10e566d5..baddad173f0fc3 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AppleCertificatePal.TempExportPal.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AppleCertificatePal.TempExportPal.cs @@ -37,8 +37,8 @@ public void Dispose() public byte[] PublicKeyValue => _realPal.PublicKeyValue; public byte[] SerialNumber => _realPal.SerialNumber; public string SignatureAlgorithm => _realPal.SignatureAlgorithm; - public DateTime NotAfter => _realPal.NotAfter; - public DateTime NotBefore => _realPal.NotBefore; + public DateTimeOffset NotAfter => _realPal.NotAfter; + public DateTimeOffset NotBefore => _realPal.NotBefore; public byte[] RawData => _realPal.RawData; public byte[] Export(X509ContentType contentType, SafePasswordHandle password) => _realPal.Export(contentType, password); diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AppleCertificatePal.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AppleCertificatePal.cs index 397d1f530008e0..e92cdd9a9de494 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AppleCertificatePal.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AppleCertificatePal.cs @@ -274,21 +274,21 @@ public byte[] RawData } } - public DateTime NotAfter + public DateTimeOffset NotAfter { get { EnsureCertData(); - return _certData.NotAfter.ToLocalTime(); + return _certData.NotAfter; } } - public DateTime NotBefore + public DateTimeOffset NotBefore { get { EnsureCertData(); - return _certData.NotBefore.ToLocalTime(); + return _certData.NotBefore; } } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateData.ManagedDecode.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateData.ManagedDecode.cs index dee76d946a78f9..edd38dd9673f9e 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateData.ManagedDecode.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateData.ManagedDecode.cs @@ -53,9 +53,9 @@ public AlgorithmIdentifier(AlgorithmIdentifierAsn algorithmIdentifier) internal byte[] SerialNumber => certificate.TbsCertificate.SerialNumber.ToArray(); - internal DateTime NotBefore => certificate.TbsCertificate.Validity.NotBefore.GetValue().UtcDateTime; + internal DateTimeOffset NotBefore => certificate.TbsCertificate.Validity.NotBefore.GetValue(); - internal DateTime NotAfter => certificate.TbsCertificate.Validity.NotAfter.GetValue().UtcDateTime; + internal DateTimeOffset NotAfter => certificate.TbsCertificate.Validity.NotAfter.GetValue(); internal AlgorithmIdentifier PublicKeyAlgorithm => new AlgorithmIdentifier(certificate.TbsCertificate.SubjectPublicKeyInfo.Algorithm); diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificatePal.Windows.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificatePal.Windows.cs index 2ab751fc49ee0e..bcc59dcef54878 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificatePal.Windows.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificatePal.Windows.cs @@ -217,24 +217,24 @@ public string SignatureAlgorithm } } - public DateTime NotAfter + public DateTimeOffset NotAfter { get { unsafe { - return InvokeWithCertContext(static pCertContext => pCertContext->pCertInfo->NotAfter.ToDateTime()); + return InvokeWithCertContext(static pCertContext => pCertContext->pCertInfo->NotAfter.ToDateTimeOffset()); } } } - public DateTime NotBefore + public DateTimeOffset NotBefore { get { unsafe { - return InvokeWithCertContext(static pCertContext => pCertContext->pCertInfo->NotBefore.ToDateTime()); + return InvokeWithCertContext(static pCertContext => pCertContext->pCertInfo->NotBefore.ToDateTimeOffset()); } } } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRequest.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRequest.cs index ef7b386da0ed5e..fc955418012cfc 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRequest.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificateRequest.cs @@ -842,33 +842,30 @@ public X509Certificate2 Create( nameof(issuerCertificate)); } - DateTime notBeforeLocal = notBefore.LocalDateTime; - if (notBeforeLocal < issuerCertificate.NotBefore) + if (notBefore < issuerCertificate.GetNotBeforeUtc()) { throw new ArgumentException( SR.Format( SR.Cryptography_CertReq_NotBeforeNotNested, - notBeforeLocal, + notBefore.LocalDateTime, issuerCertificate.NotBefore), nameof(notBefore)); } - DateTime notAfterLocal = notAfter.LocalDateTime; - // Round down to the second, since that's the cert accuracy. // This makes one method which uses the same DateTimeOffset for chained notAfters // not need to do the rounding locally. - long notAfterLocalTicks = notAfterLocal.Ticks; - long fractionalSeconds = notAfterLocalTicks % TimeSpan.TicksPerSecond; - notAfterLocalTicks -= fractionalSeconds; - notAfterLocal = new DateTime(notAfterLocalTicks, notAfterLocal.Kind); + long notAfterTicks = notAfter.Ticks; + long fractionalSeconds = notAfterTicks % TimeSpan.TicksPerSecond; + notAfterTicks -= fractionalSeconds; + notAfter = new DateTimeOffset(notAfterTicks, notAfter.Offset); - if (notAfterLocal > issuerCertificate.NotAfter) + if (notAfter > issuerCertificate.GetNotAfterUtc()) { throw new ArgumentException( SR.Format( SR.Cryptography_CertReq_NotAfterNotNested, - notAfterLocal, + notAfter.LocalDateTime, issuerCertificate.NotAfter), nameof(notAfter)); } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ChainPal.Apple.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ChainPal.Apple.cs index da6a16cf5cc9e4..d0b50a162fac8a 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ChainPal.Apple.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ChainPal.Apple.cs @@ -21,7 +21,7 @@ internal sealed class SecTrustChainPal : IChainPal private SafeX509ChainHandle? _chainHandle; public X509ChainElement[]? ChainElements { get; private set; } public X509ChainStatus[]? ChainStatus { get; private set; } - private DateTime _verificationTime; + private DateTimeOffset _verificationTime; private X509RevocationMode _revocationMode; internal SecTrustChainPal() diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ICertificatePalCore.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ICertificatePalCore.cs index 7bc6e5a0655c0a..d3e6392a08986d 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ICertificatePalCore.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ICertificatePalCore.cs @@ -20,8 +20,8 @@ internal interface ICertificatePalCore : IDisposable byte[] PublicKeyValue { get; } byte[] SerialNumber { get; } string SignatureAlgorithm { get; } - DateTime NotAfter { get; } - DateTime NotBefore { get; } + DateTimeOffset NotAfter { get; } + DateTimeOffset NotBefore { get; } byte[] RawData { get; } byte[] Export(X509ContentType contentType, SafePasswordHandle password); byte[] ExportPkcs12(Pkcs12ExportPbeParameters exportParameters, SafePasswordHandle password); diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ManagedCertificateFinder.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ManagedCertificateFinder.cs index 20fd8740a4a6a2..7f62be887d07ad 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ManagedCertificateFinder.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ManagedCertificateFinder.cs @@ -110,39 +110,32 @@ public void FindBySerialNumber(BigInteger hexValue, BigInteger decimalValue) }); } - private static DateTime NormalizeDateTime(DateTime dateTime) + private static DateTimeOffset NormalizeDateTime(DateTime dateTime) { - // If it's explicitly UTC, convert it to local before a comparison, since the - // NotBefore and NotAfter are in Local time. - // - // If it was Unknown, assume it was Local. - if (dateTime.Kind == DateTimeKind.Utc) - { - return dateTime.ToLocalTime(); - } - + // DateTime.Kind.Unspecified is assumed to be Local, matching the implicit + // DateTime -> DateTimeOffset conversion. return dateTime; } public void FindByTimeValid(DateTime dateTime) { - DateTime normalized = NormalizeDateTime(dateTime); + DateTimeOffset normalized = NormalizeDateTime(dateTime); - FindCore(normalized, static (normalized, cert) => cert.NotBefore <= normalized && normalized <= cert.NotAfter); + FindCore(normalized, static (normalized, cert) => cert.GetNotBeforeUtc() <= normalized && normalized <= cert.GetNotAfterUtc()); } public void FindByTimeNotYetValid(DateTime dateTime) { - DateTime normalized = NormalizeDateTime(dateTime); + DateTimeOffset normalized = NormalizeDateTime(dateTime); - FindCore(normalized, static (normalized, cert) => cert.NotBefore > normalized); + FindCore(normalized, static (normalized, cert) => cert.GetNotBeforeUtc() > normalized); } public void FindByTimeExpired(DateTime dateTime) { - DateTime normalized = NormalizeDateTime(dateTime); + DateTimeOffset normalized = NormalizeDateTime(dateTime); - FindCore(normalized, static (normalized, cert) => cert.NotAfter < normalized); + FindCore(normalized, static (normalized, cert) => cert.GetNotAfterUtc() < normalized); } public void FindByTemplateName(string templateName) diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/OpenSslCrlCache.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/OpenSslCrlCache.cs index 73ef5427eda10f..1fc3cf7d4dbd86 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/OpenSslCrlCache.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/OpenSslCrlCache.cs @@ -33,14 +33,14 @@ public static void AddCrlForCertificate( SafeX509Handle cert, SafeX509StoreHandle store, X509RevocationMode revocationMode, - DateTime verificationTime, + DateTimeOffset verificationTime, 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" if (revocationMode != X509RevocationMode.Online) { - verificationTime = DateTime.MinValue; + verificationTime = DateTimeOffset.MinValue; } string? url = GetCdpUrl(cert); @@ -76,14 +76,8 @@ public static void AddCrlForCertificate( DownloadAndAddCrl(url, crlFileName, store, downloadTimeout); } - private static bool AddCachedCrl(string crlFileName, SafeX509StoreHandle store, DateTime verificationTime) + private static bool AddCachedCrl(string crlFileName, SafeX509StoreHandle store, DateTimeOffset verificationTime) { - // OpenSSL is going to convert our input time to universal, so we should be in Local or - // Unspecified (local-assumed). - Debug.Assert( - verificationTime.Kind != DateTimeKind.Utc, - "UTC verificationTime should have been normalized to Local"); - if (s_crlCache.TryGetValueAndUpRef(crlFileName, out CachedCrlEntry? cacheEntry)) { try @@ -94,7 +88,7 @@ private static bool AddCachedCrl(string crlFileName, SafeX509StoreHandle store, { if (OpenSslX509ChainEventSource.Log.IsEnabled()) { - OpenSslX509ChainEventSource.Log.CrlCacheInMemoryHit(cacheEntry.Expiration); + OpenSslX509ChainEventSource.Log.CrlCacheInMemoryHit(cacheEntry.Expiration.UtcDateTime); } AttachCrl(store, cacheEntry.CrlHandle); @@ -103,7 +97,7 @@ private static bool AddCachedCrl(string crlFileName, SafeX509StoreHandle store, if (OpenSslX509ChainEventSource.Log.IsEnabled()) { - OpenSslX509ChainEventSource.Log.CrlCacheInMemoryExpired(verificationTime, cacheEntry.Expiration); + OpenSslX509ChainEventSource.Log.CrlCacheInMemoryExpired(verificationTime.UtcDateTime, cacheEntry.Expiration.UtcDateTime); } } finally @@ -165,7 +159,7 @@ private static void AttachCrl(SafeX509StoreHandle store, SafeX509CrlHandle crl) } } - private static CachedCrlEntry? CheckDiskCache(string crlFileName, DateTime verificationTime) + private static CachedCrlEntry? CheckDiskCache(string crlFileName, DateTimeOffset verificationTime) { string crlFile = GetCachedCrlPath(crlFileName); @@ -187,7 +181,7 @@ private static void AttachCrl(SafeX509StoreHandle store, SafeX509CrlHandle crl) } } - private static CachedCrlEntry? CheckDiskCacheCore(string crlFile, DateTime verificationTime) + private static CachedCrlEntry? CheckDiskCacheCore(string crlFile, DateTimeOffset verificationTime) { using (SafeBioHandle bio = Interop.Crypto.BioNewFile(crlFile, "rb")) { @@ -223,7 +217,7 @@ private static void AttachCrl(SafeX509StoreHandle store, SafeX509CrlHandle crl) // // If crl.NextUpdate is in the past, try downloading a newer version. IntPtr nextUpdatePtr = Interop.Crypto.GetX509CrlNextUpdate(crl); - DateTime nextUpdate; + DateTimeOffset nextUpdate; // If there is no crl.NextUpdate, this indicates that the CA is not providing // any more updates to the CRL, or they made a mistake not providing a NextUpdate. @@ -237,7 +231,7 @@ private static void AttachCrl(SafeX509StoreHandle store, SafeX509CrlHandle crl) try { - nextUpdate = ExpirationTimeFromCacheFileTime(File.GetLastWriteTime(crlFile)); + nextUpdate = ExpirationTimeFromCacheFileTime(File.GetLastWriteTimeUtc(crlFile)); } catch { @@ -259,7 +253,7 @@ private static void AttachCrl(SafeX509StoreHandle store, SafeX509CrlHandle crl) { if (OpenSslX509ChainEventSource.Log.IsEnabled()) { - OpenSslX509ChainEventSource.Log.CrlCacheExpired(verificationTime, nextUpdate); + OpenSslX509ChainEventSource.Log.CrlCacheExpired(verificationTime.UtcDateTime, nextUpdate.UtcDateTime); } crl.Dispose(); @@ -268,7 +262,7 @@ private static void AttachCrl(SafeX509StoreHandle store, SafeX509CrlHandle crl) if (OpenSslX509ChainEventSource.Log.IsEnabled()) { - OpenSslX509ChainEventSource.Log.CrlCacheAcceptedFile(nextUpdate); + OpenSslX509ChainEventSource.Log.CrlCacheAcceptedFile(nextUpdate.UtcDateTime); } return new CachedCrlEntry(crl, nextUpdate); @@ -305,14 +299,14 @@ private static void DownloadAndAddCrl( } IntPtr nextUpdatePtr = Interop.Crypto.GetX509CrlNextUpdate(crl); - DateTime expiryTime; + DateTimeOffset expiryTime; // If there is no crl.NextUpdate, this indicates that the CA is not providing // any more updates to the CRL, or they made a mistake not providing a NextUpdate. // We'll cache it for a few days to cover the case it was a mistake. if (nextUpdatePtr == IntPtr.Zero) { - expiryTime = ExpirationTimeFromCacheFileTime(DateTime.Now); + expiryTime = ExpirationTimeFromCacheFileTime(DateTimeOffset.UtcNow); } else { @@ -352,7 +346,7 @@ private static void DownloadAndAddCrl( return new CachedCrlEntry(crl, expiryTime); } - private static DateTime ExpirationTimeFromCacheFileTime(DateTime cacheFileTime) + private static DateTimeOffset ExpirationTimeFromCacheFileTime(DateTimeOffset cacheFileTime) { // CA/Browser Forum says that CRLs should be updated every 4 to 7 days, // so recheck any cached CRL, that doesn't have a NextUpdate, every 3 days. @@ -766,9 +760,9 @@ internal GCWatcher(MruCrlCache owner) private sealed class CachedCrlEntry { internal SafeX509CrlHandle CrlHandle { get; } - internal DateTime Expiration { get; } + internal DateTimeOffset Expiration { get; } - internal CachedCrlEntry(SafeX509CrlHandle crlHandle, DateTime expiration) + internal CachedCrlEntry(SafeX509CrlHandle crlHandle, DateTimeOffset expiration) { CrlHandle = crlHandle; Expiration = expiration; diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/OpenSslX509CertificateReader.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/OpenSslX509CertificateReader.cs index bcd55e84f5e7a0..d012e344d3c359 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/OpenSslX509CertificateReader.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/OpenSslX509CertificateReader.cs @@ -352,7 +352,7 @@ public string SignatureAlgorithm } } - public DateTime NotAfter + public DateTimeOffset NotAfter { get { @@ -363,7 +363,7 @@ public DateTime NotAfter } } - public DateTime NotBefore + public DateTimeOffset NotBefore { get { @@ -858,7 +858,7 @@ internal OpenSslX509CertificateReader DuplicateHandles() return duplicate; } - internal static unsafe DateTime ExtractValidityDateTime(IntPtr validityDatePtr) + internal static unsafe DateTimeOffset ExtractValidityDateTime(IntPtr validityDatePtr) { byte[] bytes = Interop.Crypto.GetAsn1StringBytes(validityDatePtr); @@ -903,7 +903,9 @@ internal static unsafe DateTime ExtractValidityDateTime(IntPtr validityDatePtr) DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out DateTime time)) { - return time.ToLocalTime(); + // AdjustToUniversal guarantees Kind == Utc, so this is an exact UTC instant, not a guess. + Debug.Assert(time.Kind == DateTimeKind.Utc); + return new DateTimeOffset(time); } } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/OpenSslX509ChainProcessor.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/OpenSslX509ChainProcessor.cs index 13c437617b5a36..51c5bcfc44940b 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/OpenSslX509ChainProcessor.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/OpenSslX509ChainProcessor.cs @@ -401,7 +401,7 @@ internal void ProcessRevocation( cert, _store, revocationMode, - _verificationTime.LocalDateTime, + _verificationTime, _downloadTimeout); } } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509Certificate.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509Certificate.cs index 8d38567ab282ea..c59058f9f839fb 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509Certificate.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509Certificate.cs @@ -24,8 +24,8 @@ public partial class X509Certificate : IDisposable, IDeserializationCallback, IS private byte[]? _lazyPublicKey; private byte[]? _lazyRawData; private volatile bool _lazyKeyAlgorithmParametersCreated; - private DateTime _lazyNotBefore = DateTime.MinValue; - private DateTime _lazyNotAfter = DateTime.MinValue; + private DateTimeOffset _lazyNotBefore = DateTimeOffset.MinValue; + private DateTimeOffset _lazyNotAfter = DateTimeOffset.MinValue; public virtual void Reset() { @@ -37,8 +37,8 @@ public virtual void Reset() _lazyKeyAlgorithmParameters = null; _lazyPublicKey = null; _lazyRawData = null; - _lazyNotBefore = DateTime.MinValue; - _lazyNotAfter = DateTime.MinValue; + _lazyNotBefore = DateTimeOffset.MinValue; + _lazyNotAfter = DateTimeOffset.MinValue; _lazyKeyAlgorithmParametersCreated = false; ICertificatePalCore? pal = Pal; @@ -476,12 +476,12 @@ private byte[] GetRawCertHash() public virtual string GetEffectiveDateString() { - return GetNotBefore().ToString(); + return GetNotBeforeUtc().LocalDateTime.ToString(); } public virtual string GetExpirationDateString() { - return GetNotAfter().ToString(); + return GetNotAfterUtc().LocalDateTime.ToString(); } public virtual string GetFormat() @@ -635,13 +635,13 @@ public virtual string ToString(bool fVerbose) sb.AppendLine(); sb.AppendLine("[Not Before]"); sb.Append(" "); - sb.AppendLine(FormatDate(GetNotBefore())); + sb.AppendLine(FormatDate(GetNotBeforeUtc().LocalDateTime)); // NotAfter sb.AppendLine(); sb.AppendLine("[Not After]"); sb.Append(" "); - sb.AppendLine(FormatDate(GetNotAfter())); + sb.AppendLine(FormatDate(GetNotAfterUtc().LocalDateTime)); // Thumbprint sb.AppendLine(); @@ -693,13 +693,13 @@ public virtual void Import(string fileName, SecureString? password, X509KeyStora internal ICertificatePalCore? Pal { get; private set; } - internal DateTime GetNotAfter() + internal DateTimeOffset GetNotAfterUtc() { ThrowIfInvalid(); - DateTime notAfter = _lazyNotAfter; + DateTimeOffset notAfter = _lazyNotAfter; - if (notAfter == DateTime.MinValue) + if (notAfter == DateTimeOffset.MinValue) { notAfter = _lazyNotAfter = Pal.NotAfter; } @@ -707,13 +707,13 @@ internal DateTime GetNotAfter() return notAfter; } - internal DateTime GetNotBefore() + internal DateTimeOffset GetNotBeforeUtc() { ThrowIfInvalid(); - DateTime notBefore = _lazyNotBefore; + DateTimeOffset notBefore = _lazyNotBefore; - if (notBefore == DateTime.MinValue) + if (notBefore == DateTimeOffset.MinValue) { notBefore = _lazyNotBefore = Pal.NotBefore; } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509Certificate2.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509Certificate2.cs index 74a408ce2461b5..6bf74def405289 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509Certificate2.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509Certificate2.cs @@ -305,8 +305,8 @@ public X500DistinguishedName IssuerName } } - public DateTime NotAfter => GetNotAfter(); - public DateTime NotBefore => GetNotBefore(); + public DateTime NotAfter => GetNotAfterUtc().LocalDateTime; + public DateTime NotBefore => GetNotBeforeUtc().LocalDateTime; public PublicKey PublicKey { From e1a264e136bdd9663cde0f7dbe9c7b91d4ae5893 Mon Sep 17 00:00:00 2001 From: Koen van Leeuwen Date: Fri, 26 Jun 2026 09:19:58 +0200 Subject: [PATCH 3/5] Change IFindPal time-based Find methods to DateTimeOffset instants FindByTimeValid/NotYetValid/Expired were the last internal PAL surface still typed as DateTime for a validity comparison, requiring ManagedCertificateFinder to keep an explicit Local-vs-Utc normalization shim around an otherwise UTC-based comparison. Retype the interface and its implementations (Windows native find, ManagedCertificateFinder shared by Android/Apple/OpenSSL) to take DateTimeOffset directly, deleting the now-unnecessary normalization helper. The public X509Certificate2Collection.Find(...) contract is unchanged: callers still pass a DateTime, converted to DateTimeOffset at the FindPal.cs boundary via the implicit operator. --- .../X509Certificates/FindPal.Windows.cs | 10 ++++---- .../Cryptography/X509Certificates/IFindPal.cs | 6 ++--- .../ManagedCertificateFinder.cs | 25 +++++-------------- 3 files changed, 14 insertions(+), 27 deletions(-) diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/FindPal.Windows.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/FindPal.Windows.cs index de742470c4ba4e..9ac4fe2a6e1120 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/FindPal.Windows.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/FindPal.Windows.cs @@ -107,13 +107,13 @@ public unsafe void FindBySerialNumber(BigInteger hexValue, BigInteger decimalVal }); } - public void FindByTimeValid(DateTime dateTime) => FindByTime(dateTime, 0); - public void FindByTimeNotYetValid(DateTime dateTime) => FindByTime(dateTime, -1); - public void FindByTimeExpired(DateTime dateTime) => FindByTime(dateTime, 1); + public void FindByTimeValid(DateTimeOffset instant) => FindByTime(instant, 0); + public void FindByTimeNotYetValid(DateTimeOffset instant) => FindByTime(instant, -1); + public void FindByTimeExpired(DateTimeOffset instant) => FindByTime(instant, 1); - private unsafe void FindByTime(DateTime dateTime, int compareResult) + private unsafe void FindByTime(DateTimeOffset instant, int compareResult) { - Interop.Crypt32.FILETIME fileTime = Interop.Crypt32.FILETIME.FromDateTime(dateTime); + Interop.Crypt32.FILETIME fileTime = Interop.Crypt32.FILETIME.FromDateTime(instant.UtcDateTime); FindCore( (fileTime, compareResult), diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/IFindPal.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/IFindPal.cs index 13173cd06e5585..279c9674cf08ea 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/IFindPal.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/IFindPal.cs @@ -15,9 +15,9 @@ internal interface IFindPal : IDisposable void FindByIssuerName(string issuerName); void FindByIssuerDistinguishedName(string issuerDistinguishedName); void FindBySerialNumber(BigInteger hexValue, BigInteger decimalValue); - void FindByTimeValid(DateTime dateTime); - void FindByTimeNotYetValid(DateTime dateTime); - void FindByTimeExpired(DateTime dateTime); + void FindByTimeValid(DateTimeOffset instant); + void FindByTimeNotYetValid(DateTimeOffset instant); + void FindByTimeExpired(DateTimeOffset instant); void FindByTemplateName(string templateName); void FindByApplicationPolicy(string oidValue); void FindByCertificatePolicy(string oidValue); diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ManagedCertificateFinder.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ManagedCertificateFinder.cs index 7f62be887d07ad..9329799476e5e7 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ManagedCertificateFinder.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ManagedCertificateFinder.cs @@ -110,32 +110,19 @@ public void FindBySerialNumber(BigInteger hexValue, BigInteger decimalValue) }); } - private static DateTimeOffset NormalizeDateTime(DateTime dateTime) + public void FindByTimeValid(DateTimeOffset instant) { - // DateTime.Kind.Unspecified is assumed to be Local, matching the implicit - // DateTime -> DateTimeOffset conversion. - return dateTime; + FindCore(instant, static (instant, cert) => cert.GetNotBeforeUtc() <= instant && instant <= cert.GetNotAfterUtc()); } - public void FindByTimeValid(DateTime dateTime) + public void FindByTimeNotYetValid(DateTimeOffset instant) { - DateTimeOffset normalized = NormalizeDateTime(dateTime); - - FindCore(normalized, static (normalized, cert) => cert.GetNotBeforeUtc() <= normalized && normalized <= cert.GetNotAfterUtc()); - } - - public void FindByTimeNotYetValid(DateTime dateTime) - { - DateTimeOffset normalized = NormalizeDateTime(dateTime); - - FindCore(normalized, static (normalized, cert) => cert.GetNotBeforeUtc() > normalized); + FindCore(instant, static (instant, cert) => cert.GetNotBeforeUtc() > instant); } - public void FindByTimeExpired(DateTime dateTime) + public void FindByTimeExpired(DateTimeOffset instant) { - DateTimeOffset normalized = NormalizeDateTime(dateTime); - - FindCore(normalized, static (normalized, cert) => cert.GetNotAfterUtc() < normalized); + FindCore(instant, static (instant, cert) => cert.GetNotAfterUtc() < instant); } public void FindByTemplateName(string templateName) From b6be890883d4de9e519e442f4455a74ec5bb2ea9 Mon Sep 17 00:00:00 2001 From: Koen van Leeuwen Date: Fri, 26 Jun 2026 15:16:19 +0200 Subject: [PATCH 4/5] Implement as 64bit primitive to prevent torn reads --- .../X509Certificates/X509Certificate.cs | 37 +++++++++++++------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509Certificate.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509Certificate.cs index c59058f9f839fb..08ca129e89f95c 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509Certificate.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509Certificate.cs @@ -8,6 +8,7 @@ using System.Runtime.Serialization; using System.Runtime.Versioning; using System.Text; +using System.Threading; using Internal.Cryptography; using Microsoft.Win32.SafeHandles; @@ -24,8 +25,13 @@ public partial class X509Certificate : IDisposable, IDeserializationCallback, IS private byte[]? _lazyPublicKey; private byte[]? _lazyRawData; private volatile bool _lazyKeyAlgorithmParametersCreated; - private DateTimeOffset _lazyNotBefore = DateTimeOffset.MinValue; - private DateTimeOffset _lazyNotAfter = DateTimeOffset.MinValue; + + // Cached as UTC ticks (read/written via Volatile) rather than DateTimeOffset directly: + // DateTimeOffset is larger than a pointer-sized atomic, so unsynchronized access could + // observe a torn value. 0 (DateTimeOffset.MinValue's UTC ticks) means "not yet cached", + // since no real certificate has a validity date in year 1. + private long _lazyNotBeforeUtcTicks; + private long _lazyNotAfterUtcTicks; public virtual void Reset() { @@ -37,8 +43,8 @@ public virtual void Reset() _lazyKeyAlgorithmParameters = null; _lazyPublicKey = null; _lazyRawData = null; - _lazyNotBefore = DateTimeOffset.MinValue; - _lazyNotAfter = DateTimeOffset.MinValue; + Volatile.Write(ref _lazyNotBeforeUtcTicks, 0); + Volatile.Write(ref _lazyNotAfterUtcTicks, 0); _lazyKeyAlgorithmParametersCreated = false; ICertificatePalCore? pal = Pal; @@ -697,27 +703,34 @@ internal DateTimeOffset GetNotAfterUtc() { ThrowIfInvalid(); - DateTimeOffset notAfter = _lazyNotAfter; + long ticks = Volatile.Read(ref _lazyNotAfterUtcTicks); - if (notAfter == DateTimeOffset.MinValue) + if (ticks == 0) { - notAfter = _lazyNotAfter = Pal.NotAfter; + DateTimeOffset notAfter = Pal.NotAfter; + ticks = notAfter.UtcTicks; + Volatile.Write(ref _lazyNotAfterUtcTicks, ticks); + return notAfter; } - return notAfter; + return new DateTimeOffset(ticks, TimeSpan.Zero); } internal DateTimeOffset GetNotBeforeUtc() { ThrowIfInvalid(); - DateTimeOffset notBefore = _lazyNotBefore; + long ticks = Volatile.Read(ref _lazyNotBeforeUtcTicks); - if (notBefore == DateTimeOffset.MinValue) + if (ticks == 0) { - notBefore = _lazyNotBefore = Pal.NotBefore; + DateTimeOffset notBefore = Pal.NotBefore; + ticks = notBefore.UtcTicks; + Volatile.Write(ref _lazyNotBeforeUtcTicks, ticks); + return notBefore; } - return notBefore; + + return new DateTimeOffset(ticks, TimeSpan.Zero); } [MemberNotNull(nameof(Pal))] From 065f1f4f03c563e9dd0d89e13f4e7b2b696b511f Mon Sep 17 00:00:00 2001 From: Koen van Leeuwen Date: Fri, 26 Jun 2026 15:52:18 +0200 Subject: [PATCH 5/5] Remove the last DateTime --- .../Interop/OSX/Interop.CoreFoundation.CFDate.cs | 13 ++----------- .../Interop/Windows/Crypt32/Interop.CERT_INFO.cs | 10 ++-------- .../X509Certificates/ChainPal.Android.cs | 6 +++--- .../Cryptography/X509Certificates/ChainPal.Apple.cs | 13 +++---------- .../X509Certificates/ChainPal.NotSupported.cs | 2 +- .../X509Certificates/ChainPal.OpenSsl.cs | 8 +++----- .../X509Certificates/ChainPal.Windows.BuildChain.cs | 4 ++-- .../Cryptography/X509Certificates/ChainPal.cs | 2 +- .../X509Certificates/FindPal.Windows.cs | 2 +- 9 files changed, 18 insertions(+), 42 deletions(-) diff --git a/src/libraries/Common/src/Interop/OSX/Interop.CoreFoundation.CFDate.cs b/src/libraries/Common/src/Interop/OSX/Interop.CoreFoundation.CFDate.cs index b1100a041f2fc5..a2303a72d7faa0 100644 --- a/src/libraries/Common/src/Interop/OSX/Interop.CoreFoundation.CFDate.cs +++ b/src/libraries/Common/src/Interop/OSX/Interop.CoreFoundation.CFDate.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Diagnostics; using System.Runtime.InteropServices; using Microsoft.Win32.SafeHandles; @@ -20,17 +19,9 @@ internal static partial class CoreFoundation [LibraryImport(Libraries.CoreFoundationLibrary)] private static partial SafeCFDateHandle CFDateCreate(IntPtr zero, CFAbsoluteTime at); - internal static SafeCFDateHandle CFDateCreate(DateTime date) + internal static SafeCFDateHandle CFDateCreate(DateTimeOffset date) { - Debug.Assert( - date.Kind != DateTimeKind.Unspecified, - "DateTimeKind.Unspecified should be specified to Local or UTC by the caller"); - - // UTC stays unchanged, Local is changed. - // Unspecified gets treated as Local (which may or may not be desired). - DateTime utcDate = date.ToUniversalTime(); - - double epochDeltaSeconds = (utcDate - s_cfDateEpoch).TotalSeconds; + double epochDeltaSeconds = (date.UtcDateTime - s_cfDateEpoch).TotalSeconds; SafeCFDateHandle cfDate = CFDateCreate(IntPtr.Zero, epochDeltaSeconds); diff --git a/src/libraries/Common/src/Interop/Windows/Crypt32/Interop.CERT_INFO.cs b/src/libraries/Common/src/Interop/Windows/Crypt32/Interop.CERT_INFO.cs index 29a4009741d9b3..92afbc285f4024 100644 --- a/src/libraries/Common/src/Interop/Windows/Crypt32/Interop.CERT_INFO.cs +++ b/src/libraries/Common/src/Interop/Windows/Crypt32/Interop.CERT_INFO.cs @@ -31,21 +31,15 @@ internal struct FILETIME internal uint ftTimeLow; internal uint ftTimeHigh; - internal DateTime ToDateTime() - { - long fileTime = (((long)ftTimeHigh) << 32) + ftTimeLow; - return DateTime.FromFileTime(fileTime); - } - internal DateTimeOffset ToDateTimeOffset() { long fileTime = (((long)ftTimeHigh) << 32) + ftTimeLow; return new DateTimeOffset(DateTime.FromFileTimeUtc(fileTime)); } - internal static FILETIME FromDateTime(DateTime dt) + internal static FILETIME FromDateTimeOffset(DateTimeOffset instant) { - long fileTime = dt.ToFileTime(); + long fileTime = instant.UtcDateTime.ToFileTimeUtc(); unchecked { diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ChainPal.Android.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ChainPal.Android.cs index 4e75aa66429f12..db866e7f664f18 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ChainPal.Android.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ChainPal.Android.cs @@ -34,7 +34,7 @@ internal static partial bool ReleaseSafeX509ChainHandle(IntPtr handle) X509RevocationFlag revocationFlag, X509Certificate2Collection? customTrustStore, X509ChainTrustMode trustMode, - DateTime verificationTime, + DateTimeOffset verificationTime, TimeSpan timeout, bool disableAia) { @@ -182,7 +182,7 @@ internal void Initialize( } internal void Evaluate( - DateTime verificationTime, + DateTimeOffset verificationTime, OidCollection? applicationPolicy, OidCollection? certificatePolicy, X509RevocationMode revocationMode, @@ -190,7 +190,7 @@ internal void Evaluate( { Debug.Assert(_chainContext != null); - long timeInMsFromUnixEpoch = new DateTimeOffset(verificationTime).ToUnixTimeMilliseconds(); + long timeInMsFromUnixEpoch = verificationTime.ToUnixTimeMilliseconds(); _isValid = Interop.AndroidCrypto.X509ChainBuild(_chainContext, timeInMsFromUnixEpoch); if (!_isValid) { diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ChainPal.Apple.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ChainPal.Apple.cs index d0b50a162fac8a..0e625f1d086b9c 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ChainPal.Apple.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ChainPal.Apple.cs @@ -222,7 +222,7 @@ private SafeCreateHandle GetCertsArray(List safeHandles) } internal void Execute( - DateTime verificationTime, + DateTimeOffset verificationTime, bool allowNetwork, OidCollection? applicationPolicy, OidCollection? certificatePolicy, @@ -478,7 +478,7 @@ private X509ChainStatus[] BuildChainElementStatuses(X509Certificate2? cert, int const int errSecCertificateExpired = -67818; const int errSecCertificateNotValidYet = -67819; - osStatus = cert != null && cert.NotBefore > _verificationTime ? + osStatus = cert != null && cert.GetNotBeforeUtc() > _verificationTime ? errSecCertificateNotValidYet : errSecCertificateExpired; errorString = Interop.AppleCrypto.GetSecErrorString(osStatus); @@ -593,17 +593,10 @@ internal static partial bool ReleaseSafeX509ChainHandle(IntPtr handle) X509RevocationFlag revocationFlag, X509Certificate2Collection? customTrustStore, X509ChainTrustMode trustMode, - DateTime verificationTime, + DateTimeOffset verificationTime, TimeSpan timeout, bool disableAia) { - // If the time was given in Universal, it will stay Universal. - // If the time was given in Local, it will be converted. - // If the time was given in Unspecified, it will be assumed local, and converted. - // - // This matches the "assume Local unless explicitly Universal" implicit contract. - verificationTime = verificationTime.ToUniversalTime(); - SecTrustChainPal chainPal = new SecTrustChainPal(); // The allowNetwork controls all network activity for macOS chain building. diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ChainPal.NotSupported.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ChainPal.NotSupported.cs index b687b3d4fc46a9..e62e7160f490ce 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ChainPal.NotSupported.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ChainPal.NotSupported.cs @@ -26,7 +26,7 @@ internal static partial bool ReleaseSafeX509ChainHandle(IntPtr handle) X509RevocationFlag revocationFlag, X509Certificate2Collection? customTrustStore, X509ChainTrustMode trustMode, - DateTime verificationTime, + DateTimeOffset verificationTime, TimeSpan timeout, bool disableAia) { diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ChainPal.OpenSsl.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ChainPal.OpenSsl.cs index fdd0a23487ac73..e46023ac46c744 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ChainPal.OpenSsl.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ChainPal.OpenSsl.cs @@ -37,7 +37,7 @@ public static void FlushStores() X509RevocationFlag revocationFlag, X509Certificate2Collection? customTrustStore, X509ChainTrustMode trustMode, - DateTime verificationTime, + DateTimeOffset verificationTime, TimeSpan timeout, bool disableAia) { @@ -81,7 +81,7 @@ public static void FlushStores() X509RevocationFlag revocationFlag, X509Certificate2Collection? customTrustStore, X509ChainTrustMode trustMode, - DateTime verificationTime, + DateTimeOffset verificationTime, TimeSpan timeout, bool disableAia) { @@ -98,8 +98,6 @@ public static void FlushStores() timeout = s_maxUrlRetrievalTimeout; } - DateTimeOffset verificationInstant = new DateTimeOffset(verificationTime); - // Until we support the Disallowed store, ensure it's empty (which is done by the ctor) using (new X509Store(StoreName.Disallowed, StoreLocation.CurrentUser, OpenFlags.ReadOnly)) { @@ -111,7 +109,7 @@ public static void FlushStores() ((OpenSslX509CertificateReader)cert).SafeHandle, customTrustStore, trustMode, - verificationInstant, + verificationTime, downloadTimeout); Interop.Crypto.X509VerifyStatusCode status = chainPal.FindFirstChain(extraStore); diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ChainPal.Windows.BuildChain.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ChainPal.Windows.BuildChain.cs index 8bbdf3482400ba..968936b4fc2c9b 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ChainPal.Windows.BuildChain.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ChainPal.Windows.BuildChain.cs @@ -24,7 +24,7 @@ internal sealed partial class ChainPal : IDisposable, IChainPal X509RevocationFlag revocationFlag, X509Certificate2Collection? customTrustStore, X509ChainTrustMode trustMode, - DateTime verificationTime, + DateTimeOffset verificationTime, TimeSpan timeout, bool disableAia) { @@ -60,7 +60,7 @@ internal sealed partial class ChainPal : IDisposable, IChainPal chainPara.dwUrlRetrievalTimeout = (int)Math.Floor(timeout.TotalMilliseconds); - Interop.Crypt32.FILETIME ft = Interop.Crypt32.FILETIME.FromDateTime(verificationTime); + Interop.Crypt32.FILETIME ft = Interop.Crypt32.FILETIME.FromDateTimeOffset(verificationTime); Interop.Crypt32.CertChainFlags flags = MapRevocationFlags(revocationMode, revocationFlag, disableAia); SafeX509ChainHandle chain; using (SafeCertContextHandle certContext = certificatePal.GetCertContext()) diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ChainPal.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ChainPal.cs index 2d765b6f8158de..67f40f469c078a 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ChainPal.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ChainPal.cs @@ -19,7 +19,7 @@ internal partial class ChainPal X509RevocationFlag revocationFlag, X509Certificate2Collection? customTrustStore, X509ChainTrustMode trustMode, - DateTime verificationTime, + DateTimeOffset verificationTime, TimeSpan timeout, bool disableAia); } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/FindPal.Windows.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/FindPal.Windows.cs index 9ac4fe2a6e1120..4de21118c401ef 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/FindPal.Windows.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/FindPal.Windows.cs @@ -113,7 +113,7 @@ public unsafe void FindBySerialNumber(BigInteger hexValue, BigInteger decimalVal private unsafe void FindByTime(DateTimeOffset instant, int compareResult) { - Interop.Crypt32.FILETIME fileTime = Interop.Crypt32.FILETIME.FromDateTime(instant.UtcDateTime); + Interop.Crypt32.FILETIME fileTime = Interop.Crypt32.FILETIME.FromDateTimeOffset(instant); FindCore( (fileTime, compareResult),