Skip to content

Fix OpenSSL X509Chain time validity depending on process time zone#129394

Open
koen-lee wants to merge 2 commits into
dotnet:mainfrom
koen-lee:fix-109039-utc-verifytime
Open

Fix OpenSSL X509Chain time validity depending on process time zone#129394
koen-lee wants to merge 2 commits into
dotnet:mainfrom
koen-lee:fix-109039-utc-verifytime

Conversation

@koen-lee

Copy link
Copy Markdown

Fixes #109039.

Problem

On the OpenSSL chain backend, the chain verification time was passed to native code as broken-down local fields (resolved against .NET's cached time zone) and reconstructed with mktime() (which resolves them against libc's cached zone). These are two independent zone resolutions, so a system time-zone change while the process runs can desync them and the verdict flips:

This is independent of how the caller supplied the verification time (DateTime.Now default, or an explicit VerificationTime of either Kind).

Fix

X509_VERIFY_PARAM_set_time takes a time_t, so the local round-trip is not required (as noted in the issue). Instead, pass the verification time straight through as a UTC Unix timestamp:

  • Interop.Crypto.X509StoreSetVerifyTime now takes a DateTimeOffset and passes ToUnixTimeSeconds().
  • CryptoNative_X509StoreSetVerifyTime takes an int64_t and forwards it directly to X509_VERIFY_PARAM_set_time; MakeTimeT/mktime are removed. The 32-bit-time_t ARM guard (g_libSslUses32BitTime) is preserved.
  • ChainPal.OpenSsl no longer normalizes the verification time to local.

This is an internal + native change only. X509ChainPolicy.VerificationTime remains a public DateTime; no public API changes.

Scope

Windows is unaffected; macOS already normalizes to UTC. This fix is OpenSSL/Linux-only.

OpenSslCrlCache reads cert validity through the same local-time path (ExtractValidityDateTime); that's a separate read path and considered out of scope here. I'm happy to follow up to carry DateTimeOffset through the OpenSSL path / ICertificatePal for consistency.

Tests

Two Linux-only regression test classes (RemoteExecutor + TZ environment var, so the time-zone change is isolated to a child process), each a theory over Kind=Utc/Kind=Local verification instants:

  • ChainTimeZoneTests, a valid certificate stays valid across a mid-process time-zone change.
  • ChainTimeZoneExpiredTests, an expired certificate stays NotTimeValid across a mid-process time-zone change.

I don't know how a unit test can deterministically change the process's system zone the way the field repro did (libc picks up the change, .NET stays stale), so the tests invert the desync: ClearCachedData() re-syncs .NET to the new TZ and leaves libc stale. Both tests fail before the fix and pass after.

koen-lee and others added 2 commits June 14, 2026 11:54
…ocess TZ change)

Linux-only tests (RemoteExecutor + TZ) covering both directions of the OpenSSL
chain verification-time bug, as a theory over Kind=Utc and Kind=Local verify times:
a valid certificate must stay time-valid, and an expired certificate must stay
NotTimeValid, across a mid-process time-zone change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The OpenSSL chain backend passed the verification time to native code as
broken-down local fields and reconstructed it with mktime(), which re-resolves
them against the process's current time zone. Because a certificate's validity
window is a fixed UTC instant, a mid-process time-zone change shifted the effective
verification instant and could flip the time-validity verdict (a valid cert
reported NotTimeValid, or an expired cert no longer reported NotTimeValid).

X509_VERIFY_PARAM_set_time takes a time_t (UTC epoch seconds), so the local
round-trip was unnecessary. Pass the verification time as a UTC Unix timestamp:
- Interop X509StoreSetVerifyTime now takes DateTimeOffset -> ToUnixTimeSeconds().
- CryptoNative_X509StoreSetVerifyTime takes int64_t and forwards it straight to
  X509_VERIFY_PARAM_set_time; MakeTimeT/mktime removed. The 32-bit time_t ARM
  guard is preserved.
- The OpenSSL chain processor carries the verification time as a DateTimeOffset.

Internal + native only; X509ChainPolicy.VerificationTime stays a public DateTime.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 14, 2026 17:16
@dotnet-policy-service dotnet-policy-service Bot added the community-contribution Indicates that the PR has been added by a community member label Jun 14, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR fixes X509 chain verification-time handling on Linux/OpenSSL so that certificate time validity doesn’t depend on the process time zone.

Changes:

  • Switch native verify-time API from local-time components to a Unix-time (UTC) instant.
  • Update OpenSSL chain processor to carry verification time as DateTimeOffset and pass the instant to native.
  • Add Linux-only regression tests that toggle TZ in a child process to ensure verdict stability.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/native/libs/System.Security.Cryptography.Native/openssl.h Updates native API signature to accept Unix time.
src/native/libs/System.Security.Cryptography.Native/openssl.c Implements Unix-time based verify time, removing local-time conversion.
src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ChainPal.OpenSsl.cs Converts DateTime verificationTime into a DateTimeOffset instant for OpenSSL.
src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/OpenSslX509ChainProcessor.cs Stores verification time as DateTimeOffset; adjusts revocation call site.
src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.Crypto.cs Updates P/Invoke to pass Unix time seconds to native.
src/libraries/System.Security.Cryptography/tests/X509Certificates/ChainTests.TimeZone.Linux.cs Adds regression tests for time-zone independence on Linux.
src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj Includes the new Linux time-zone test file in the test project.
Comments suppressed due to low confidence (1)

src/native/libs/System.Security.Cryptography.Native/openssl.c:1

  • Casting int64_t unixTime directly to time_t can overflow or wrap on platforms with 32-bit time_t (or for out-of-range inputs), potentially setting an incorrect verification instant rather than failing. This function used to reject invalid conversions (via mktime returning -1), but now it always proceeds. Consider validating that the cast is lossless (e.g., round-trip compare unixTime vs (int64_t)(time_t)unixTime) and returning 0 on overflow/out-of-range so callers don’t get silently wrong chain results.
// Licensed to the .NET Foundation under one or more agreements.

_store,
revocationMode,
_verificationTime,
_verificationTime.LocalDateTime,
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @bartonjs, @vcsjones, @dotnet/area-system-security
See info in area-owners.md if you want to be subscribed.

@koen-lee

Copy link
Copy Markdown
Author

@dotnet-policy-service agree company="Logiqs b.v."

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-System.Security community-contribution Indicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Certificate validation routine fails with NotTimeValid when changing time zone on Linux while client app is still running

2 participants