diff --git a/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.Crypto.cs b/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.Crypto.cs
index a451e0d9e918e3..2fef48465370d2 100644
--- a/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.Crypto.cs
+++ b/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.Crypto.cs
@@ -90,13 +90,7 @@ internal static int BioTell(SafeBioHandle bio)
[LibraryImport(Libraries.CryptoNative)]
private static partial int CryptoNative_X509StoreSetVerifyTime(
SafeX509StoreHandle ctx,
- int year,
- int month,
- int day,
- int hour,
- int minute,
- int second,
- [MarshalAs(UnmanagedType.Bool)] bool isDst);
+ long unixTime);
[LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_CheckX509IpAddress", StringMarshalling = StringMarshalling.Utf8)]
internal static partial int CheckX509IpAddress(SafeX509Handle x509, byte[] addressBytes, int addressLen, string hostname, int cchHostname);
@@ -150,21 +144,9 @@ internal static X500DistinguishedName LoadX500Name(IntPtr namePtr)
return GetNullableDynamicBuffer(GetX509PublicKeyParameterBytes, x509);
}
- internal static void X509StoreSetVerifyTime(SafeX509StoreHandle ctx, DateTime verifyTime)
+ internal static void X509StoreSetVerifyTime(SafeX509StoreHandle ctx, DateTimeOffset verifyTime)
{
- // OpenSSL is going to convert our input time to universal, so we should be in Local or
- // Unspecified (local-assumed).
- Debug.Assert(verifyTime.Kind != DateTimeKind.Utc, "UTC verifyTime should have been normalized to Local");
-
- int succeeded = CryptoNative_X509StoreSetVerifyTime(
- ctx,
- verifyTime.Year,
- verifyTime.Month,
- verifyTime.Day,
- verifyTime.Hour,
- verifyTime.Minute,
- verifyTime.Second,
- verifyTime.IsDaylightSavingTime());
+ int succeeded = CryptoNative_X509StoreSetVerifyTime(ctx, verifyTime.ToUnixTimeSeconds());
if (succeeded != 1)
{
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 2c034f6d1b4b26..fdd0a23487ac73 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
@@ -98,14 +98,7 @@ public static void FlushStores()
timeout = s_maxUrlRetrievalTimeout;
}
- // Let Unspecified mean Local, so only convert if the source was UTC.
- //
- // Converge on Local instead of UTC because OpenSSL is going to assume we gave it
- // local time.
- if (verificationTime.Kind == DateTimeKind.Utc)
- {
- verificationTime = verificationTime.ToLocalTime();
- }
+ 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))
@@ -118,7 +111,7 @@ public static void FlushStores()
((OpenSslX509CertificateReader)cert).SafeHandle,
customTrustStore,
trustMode,
- verificationTime,
+ verificationInstant,
downloadTimeout);
Interop.Crypto.X509VerifyStatusCode status = chainPal.FindFirstChain(extraStore);
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 a9883b74b2aff7..13c437617b5a36 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
@@ -46,7 +46,7 @@ internal sealed class OpenSslX509ChainProcessor : IChainPal
private SafeX509StoreHandle _store;
private readonly SafeX509StackHandle _untrustedLookup;
private readonly SafeX509StoreCtxHandle _storeCtx;
- private readonly DateTime _verificationTime;
+ private readonly DateTimeOffset _verificationTime;
private readonly TimeSpan _downloadTimeout;
private WorkingChain? _workingChain;
@@ -55,7 +55,7 @@ private OpenSslX509ChainProcessor(
SafeX509StoreHandle store,
SafeX509StackHandle untrusted,
SafeX509StoreCtxHandle storeCtx,
- DateTime verificationTime,
+ DateTimeOffset verificationTime,
TimeSpan downloadTimeout)
{
_leafHandle = leafHandle;
@@ -95,7 +95,7 @@ internal static OpenSslX509ChainProcessor InitiateChain(
SafeX509Handle leafHandle,
X509Certificate2Collection? customTrustStore,
X509ChainTrustMode trustMode,
- DateTime verificationTime,
+ DateTimeOffset verificationTime,
TimeSpan remainingDownloadTime)
{
OpenSslCachedSystemStoreProvider.GetNativeCollections(
@@ -401,7 +401,7 @@ internal void ProcessRevocation(
cert,
_store,
revocationMode,
- _verificationTime,
+ _verificationTime.LocalDateTime,
_downloadTimeout);
}
}
diff --git a/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj b/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj
index 56ed338c3f9b1a..68bc745cb6ecd2 100644
--- a/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj
+++ b/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj
@@ -632,6 +632,7 @@
+
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
new file mode 100644
index 00000000000000..eb4f636fcc0b0d
--- /dev/null
+++ b/src/libraries/System.Security.Cryptography/tests/X509Certificates/ChainTests.TimeZone.Linux.cs
@@ -0,0 +1,85 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.DotNet.RemoteExecutor;
+using Xunit;
+
+namespace System.Security.Cryptography.X509Certificates.Tests
+{
+ // Chain 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)]
+ public static class ChainTimeZoneTests
+ {
+ private static readonly DateTimeOffset s_verify = new(2024, 6, 15, 12, 0, 0, TimeSpan.Zero);
+
+ [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
+ [InlineData(false)]
+ [InlineData(true)]
+ public static void ValidCert_StaysTimeValidAfterTimeZoneChange(bool asLocal)
+ {
+ RemoteExecutor.Invoke(static asLocalStr =>
+ {
+ // Validity margin (+/-2h) is narrower than the UTC+14 shift below, so the
+ // bug (if present) moves the effective verify time outside the window.
+ using X509Certificate2 cert = MakeCert(s_verify.AddHours(-2), s_verify.AddHours(2));
+ bool asLocal = bool.Parse(asLocalStr);
+
+ SetZone("UTC");
+ Assert.True(IsTimeValid(cert, s_verify, asLocal));
+
+ SetZone("Pacific/Kiritimati"); // UTC+14
+ Assert.True(IsTimeValid(cert, s_verify, asLocal));
+ }, asLocal.ToString()).Dispose();
+ }
+
+ [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
+ [InlineData(false)]
+ [InlineData(true)]
+ public static void ExpiredCert_StaysNotTimeValidAfterTimeZoneChange(bool asLocal)
+ {
+ RemoteExecutor.Invoke(static asLocalStr =>
+ {
+ // Expired 30 min before s_verify. NotBefore is >14h earlier so the
+ // westward shift (~14h) lands back inside the window (re-entering
+ // validity from the expiry side), not before NotBefore.
+ using X509Certificate2 cert = MakeCert(s_verify.AddHours(-20), s_verify.AddMinutes(-30));
+ bool asLocal = bool.Parse(asLocalStr);
+
+ SetZone("Pacific/Kiritimati"); // UTC+14
+ Assert.False(IsTimeValid(cert, s_verify, asLocal));
+
+ SetZone("UTC"); // westward: "now" moves back ~14h
+ Assert.False(IsTimeValid(cert, s_verify, asLocal));
+ }, asLocal.ToString()).Dispose();
+ }
+
+ internal static void SetZone(string tz)
+ {
+ Environment.SetEnvironmentVariable("TZ", tz);
+ TimeZoneInfo.ClearCachedData();
+ }
+
+ internal static X509Certificate2 MakeCert(DateTimeOffset notBefore, DateTimeOffset notAfter)
+ {
+ using RSA key = RSA.Create(2048);
+ var req = new CertificateRequest("CN=109039", key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
+ return req.CreateSelfSigned(notBefore, notAfter);
+ }
+
+ // Verify-time source (Utc vs Local) must not change the verdict.
+ internal static bool IsTimeValid(X509Certificate2 cert, DateTimeOffset verify, bool asLocal)
+ {
+ using ChainHolder holder = new();
+ X509Chain chain = holder.Chain;
+ chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
+ chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
+ chain.ChainPolicy.CustomTrustStore.Add(cert);
+ chain.ChainPolicy.VerificationTime = asLocal ? verify.LocalDateTime : verify.UtcDateTime;
+
+ chain.Build(cert);
+ return (chain.AllStatusFlags() & X509ChainStatusFlags.NotTimeValid) == 0;
+ }
+ }
+}
diff --git a/src/native/libs/System.Security.Cryptography.Native/openssl.c b/src/native/libs/System.Security.Cryptography.Native/openssl.c
index 1b80f0bb03407c..d31ebf441529e9 100644
--- a/src/native/libs/System.Security.Cryptography.Native/openssl.c
+++ b/src/native/libs/System.Security.Cryptography.Native/openssl.c
@@ -38,31 +38,6 @@ c_static_assert(CRYPTO_EX_INDEX_SSL_SESSION == 2);
// See X509NameType.UrlName
#define NAME_TYPE_URL 5
-/*
-Function:
-MakeTimeT
-
-Used to convert the constituent elements of a struct tm into a time_t. As time_t does not have
-a guaranteed blitting size, this function is static and cannot be p/invoked. It is here merely
-as a utility.
-
-Return values:
-A time_t representation of the input date. See also man mktime(3).
-*/
-static time_t
-MakeTimeT(int32_t year, int32_t month, int32_t day, int32_t hour, int32_t minute, int32_t second, int32_t isDst)
-{
- struct tm currentTm;
- currentTm.tm_year = year - 1900;
- currentTm.tm_mon = month - 1;
- currentTm.tm_mday = day;
- currentTm.tm_hour = hour;
- currentTm.tm_min = minute;
- currentTm.tm_sec = second;
- currentTm.tm_isdst = isDst;
- return mktime(¤tTm);
-}
-
/*
Function:
GetX509Thumbprint
@@ -914,20 +889,14 @@ void CryptoNative_RecursiveFreeX509Stack(STACK_OF(X509) * stack)
SetX509StoreVerifyTime
Used by System.Security.Cryptography.X509Certificates' OpenSslX509ChainProcessor to assign the
-verification time to the chain building. The input is in LOCAL time, not UTC.
+verification time to the chain building. The input is an absolute instant as Unix time
+(seconds since 1970-01-01 UTC), directly convertible to a time_t value.
Return values:
-0 if ctx is NULL, if ctx has no X509_VERIFY_PARAM, or the date inputs don't produce a valid time_t;
+0 if ctx is NULL, if ctx has no X509_VERIFY_PARAM, or if the time is out of bounds on a 32-bit time environment.
1 on success.
*/
-int32_t CryptoNative_X509StoreSetVerifyTime(X509_STORE* ctx,
- int32_t year,
- int32_t month,
- int32_t day,
- int32_t hour,
- int32_t minute,
- int32_t second,
- int32_t isDst)
+int32_t CryptoNative_X509StoreSetVerifyTime(X509_STORE* ctx, int64_t unixTime)
{
ERR_clear_error();
@@ -936,12 +905,7 @@ int32_t CryptoNative_X509StoreSetVerifyTime(X509_STORE* ctx,
return 0;
}
- time_t verifyTime = MakeTimeT(year, month, day, hour, minute, second, isDst);
-
- if (verifyTime == (time_t)-1)
- {
- return 0;
- }
+ time_t verifyTime = (time_t)unixTime;
X509_VERIFY_PARAM* verifyParams = X509_STORE_get0_param(ctx);
diff --git a/src/native/libs/System.Security.Cryptography.Native/openssl.h b/src/native/libs/System.Security.Cryptography.Native/openssl.h
index d1ff2f8bb4aaf7..5cc1a4bf820445 100644
--- a/src/native/libs/System.Security.Cryptography.Native/openssl.h
+++ b/src/native/libs/System.Security.Cryptography.Native/openssl.h
@@ -42,14 +42,7 @@ PALEXPORT X509* CryptoNative_GetX509StackField(STACK_OF(X509) * stack, int loc);
PALEXPORT void CryptoNative_RecursiveFreeX509Stack(STACK_OF(X509) * stack);
-PALEXPORT int32_t CryptoNative_X509StoreSetVerifyTime(X509_STORE* ctx,
- int32_t year,
- int32_t month,
- int32_t day,
- int32_t hour,
- int32_t minute,
- int32_t second,
- int32_t isDst);
+PALEXPORT int32_t CryptoNative_X509StoreSetVerifyTime(X509_STORE* ctx, int64_t unixTime);
PALEXPORT X509* CryptoNative_ReadX509AsDerFromBio(BIO* bio);