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);