Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 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 @@ -11,7 +11,7 @@ public static class CertificateGenerator
public static void GenerateAspNetHttpsCertificate()
{
var manager = CertificateManager.Instance;
var now = DateTimeOffset.Now;
var now = DateTimeOffset.UtcNow;
manager.EnsureAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1), isInteractive: false);
}
}
100 changes: 100 additions & 0 deletions src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using Microsoft.AspNetCore.DeveloperCertificates.XPlat;
using Microsoft.AspNetCore.InternalTesting;
using Xunit;
using Xunit.Abstractions;
Expand Down Expand Up @@ -516,6 +517,105 @@ public void ListCertificates_AlwaysReturnsTheCertificate_WithHighestVersion()
e.RawData[0] == 2);
}

[Fact]
public void GenerateAspNetHttpsCertificate_UsesUtcTime_CertificateIsImmediatelyValid()
{
// This test verifies that CertificateGenerator.GenerateAspNetHttpsCertificate() uses UTC time
// instead of local time, ensuring certificates are immediately valid regardless of timezone

try
{
_fixture.CleanupCertificates();

// Record both UTC and local time before calling the method
var beforeCallUtc = DateTimeOffset.UtcNow;
var beforeCallLocal = DateTimeOffset.Now;

// Call the method that was fixed to use DateTimeOffset.UtcNow
CertificateGenerator.GenerateAspNetHttpsCertificate();

// Record both UTC and local time after calling the method
var afterCallUtc = DateTimeOffset.UtcNow;
var afterCallLocal = DateTimeOffset.Now;

// Get the certificate that was created
var certificates = _manager.ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: true);
Assert.True(certificates.Count > 0, "Expected at least one certificate to be created");

var certificate = certificates.First();

// Convert certificate NotBefore to UTC (certificates store time as local time)
var notBefore = new DateTimeOffset(certificate.NotBefore.ToUniversalTime(), TimeSpan.Zero);

// The certificate's NotBefore should be close to UTC time, not local time
// If it used DateTimeOffset.Now in a non-UTC timezone, it would be off by the timezone offset
var utcTimeDiff = Math.Abs((notBefore - beforeCallUtc).TotalSeconds);
Copy link
Member

Choose a reason for hiding this comment

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

@copilot I guess this will fail if during the test there is a daylight savings change or timezone change.. can that be made robust without too much complexity?

also, since a fault is likely to show up as a diff of several hours, I suggest using something significantly longer than 10 seconds, to allow for a very slow machine?

var localTimeDiff = Math.Abs((notBefore - beforeCallLocal.ToUniversalTime()).TotalSeconds);

// In UTC timezone, both differences would be small, but in non-UTC timezone,
// UTC difference should be much smaller than local time difference
Assert.True(utcTimeDiff <= 10,
$"Certificate NotBefore should be close to UTC time. NotBefore: {notBefore:yyyy-MM-dd HH:mm:ss} UTC, " +
$"BeforeCall UTC: {beforeCallUtc:yyyy-MM-dd HH:mm:ss} UTC, Difference: {utcTimeDiff:F2} seconds");

// Verify the certificate is immediately valid (NotBefore <= now in UTC)
var utcNow = DateTimeOffset.UtcNow;
Assert.True(notBefore <= utcNow,
$"Certificate should be immediately valid. NotBefore: {notBefore:yyyy-MM-dd HH:mm:ss} UTC, Current UTC: {utcNow:yyyy-MM-dd HH:mm:ss} UTC");

// Output diagnostic information to help understand the test
Output.WriteLine($"Certificate NotBefore: {notBefore:yyyy-MM-dd HH:mm:ss} UTC");
Output.WriteLine($"UTC time before call: {beforeCallUtc:yyyy-MM-dd HH:mm:ss} UTC");
Output.WriteLine($"Local time before call: {beforeCallLocal:yyyy-MM-dd HH:mm:ss zzz}");
Output.WriteLine($"UTC time difference: {utcTimeDiff:F2} seconds");
Output.WriteLine($"Local time difference: {localTimeDiff:F2} seconds");
}
finally
{
_fixture.CleanupCertificates();
}
}

[Fact]
public void CertificateGenerator_FixedToUseUtcNow_NotLocalNow()
{
// This test documents the fix that was made to CertificateGenerator.GenerateAspNetHttpsCertificate()
// The method was changed from DateTimeOffset.Now to DateTimeOffset.UtcNow to fix timezone issues

// Test that the method behaves correctly by ensuring the certificate uses UTC-based time
// In non-UTC timezones, using DateTimeOffset.Now would create certificates with future NotBefore timestamps

try
{
_fixture.CleanupCertificates();

// Call the fixed method
CertificateGenerator.GenerateAspNetHttpsCertificate();

// Verify a certificate was created and is immediately valid
var certificates = _manager.ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: true);
Assert.True(certificates.Count > 0, "Certificate should be created");

var certificate = certificates.First();

// The certificate should be immediately valid - NotBefore should not be in the future
Assert.True(certificate.NotBefore <= DateTime.UtcNow.AddSeconds(5),
Copy link
Member

Choose a reason for hiding this comment

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

again a v ery slow test machine will exceed 5 seconds

"Certificate NotBefore should not be in the future (which would happen with DateTimeOffset.Now in non-UTC timezones)");

// The certificate should be valid for approximately 1 year from now
var expectedExpiry = DateTime.UtcNow.AddYears(1);
var actualExpiry = certificate.NotAfter;
var expiryDiff = Math.Abs((expectedExpiry - actualExpiry).TotalDays);

Assert.True(expiryDiff <= 1,
$"Certificate should expire approximately 1 year from now. Expected: {expectedExpiry:yyyy-MM-dd}, Actual: {actualExpiry:yyyy-MM-dd}");
}
finally
{
_fixture.CleanupCertificates();
}
}

[ConditionalFact]
[OSSkipCondition(OperatingSystems.Windows, SkipReason = "UnixFileMode is not supported on Windows.")]
[OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "https://github.com/dotnet/aspnetcore/issues/6720")]
Expand Down
2 changes: 1 addition & 1 deletion src/Tools/dotnet-dev-certs/src/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ private static int CheckHttpsCertificateJsonOutput(IReporter reporter)

private static int EnsureHttpsCertificate(CommandOption exportPath, CommandOption password, CommandOption noPassword, CommandOption trust, CommandOption exportFormat, IReporter reporter)
{
var now = DateTimeOffset.Now;
var now = DateTimeOffset.UtcNow;
var manager = CertificateManager.Instance;

if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
Expand Down
Loading