Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -118,7 +111,7 @@ public static void FlushStores()
((OpenSslX509CertificateReader)cert).SafeHandle,
customTrustStore,
trustMode,
verificationTime,
verificationInstant,
downloadTimeout);

Interop.Crypto.X509VerifyStatusCode status = chainPal.FindFirstChain(extraStore);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -55,7 +55,7 @@ private OpenSslX509ChainProcessor(
SafeX509StoreHandle store,
SafeX509StackHandle untrusted,
SafeX509StoreCtxHandle storeCtx,
DateTime verificationTime,
DateTimeOffset verificationTime,
TimeSpan downloadTimeout)
{
_leafHandle = leafHandle;
Expand Down Expand Up @@ -95,7 +95,7 @@ internal static OpenSslX509ChainProcessor InitiateChain(
SafeX509Handle leafHandle,
X509Certificate2Collection? customTrustStore,
X509ChainTrustMode trustMode,
DateTime verificationTime,
DateTimeOffset verificationTime,
TimeSpan remainingDownloadTime)
{
OpenSslCachedSystemStoreProvider.GetNativeCollections(
Expand Down Expand Up @@ -401,7 +401,7 @@ internal void ProcessRevocation(
cert,
_store,
revocationMode,
_verificationTime,
_verificationTime.LocalDateTime,
Comment thread
koen-lee marked this conversation as resolved.
Comment thread
bartonjs marked this conversation as resolved.
_downloadTimeout);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,7 @@
<Compile Include="X509Certificates\ChainHolder.cs" />
<Compile Include="X509Certificates\ChainPolicyTests.cs" />
<Compile Include="X509Certificates\ChainTests.cs" />
<Compile Include="X509Certificates\ChainTests.TimeZone.Linux.cs" />
<Compile Include="X509Certificates\CollectionImportTests.cs" />
<Compile Include="X509Certificates\CollectionTests.cs" />
<Compile Include="X509Certificates\ContentTypeTests.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
46 changes: 5 additions & 41 deletions src/native/libs/System.Security.Cryptography.Native/openssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -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(&currentTm);
}

/*
Function:
GetX509Thumbprint
Expand Down Expand Up @@ -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.
Comment thread
bartonjs marked this conversation as resolved.
Comment thread
bartonjs marked this conversation as resolved.
*/
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();
Comment thread
bartonjs marked this conversation as resolved.

Expand All @@ -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;
Comment thread
bartonjs marked this conversation as resolved.

X509_VERIFY_PARAM* verifyParams = X509_STORE_get0_param(ctx);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down