Files
git.stella-ops.org/src/__Libraries/StellaOps.Doctor.Plugins.Attestation/Checks/ClockSkewCheck.cs
2026-01-16 18:39:36 +02:00

182 lines
7.5 KiB
C#

using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugins;
using StellaOps.Doctor.Plugins.Attestation.Configuration;
using StellaOps.Doctor.Plugins.Builders;
namespace StellaOps.Doctor.Plugins.Attestation.Checks;
/// <summary>
/// Verifies system clock is within acceptable range for signature verification.
/// </summary>
public sealed class ClockSkewCheck : AttestationCheckBase
{
/// <inheritdoc />
public override string CheckId => "check.attestation.clock.skew";
/// <inheritdoc />
public override string Name => "Clock Skew Sanity";
/// <inheritdoc />
public override string Description => "Verifies system clock is synchronized within acceptable range for signature verification";
/// <inheritdoc />
public override DoctorSeverity DefaultSeverity => DoctorSeverity.Warn;
/// <inheritdoc />
public override IReadOnlyList<string> Tags => ["quick", "attestation", "security", "time"];
/// <inheritdoc />
public override TimeSpan EstimatedDuration => TimeSpan.FromSeconds(3);
/// <inheritdoc />
protected override async Task<DoctorCheckResult> ExecuteCheckAsync(
DoctorPluginContext context,
AttestationPluginOptions options,
CheckResultBuilder result,
CancellationToken ct)
{
var localTime = context.TimeProvider.GetUtcNow();
TimeSpan? skew = null;
string? referenceSource = null;
DateTimeOffset? referenceTime = null;
// Try to get reference time from Rekor if available
if (options.Mode != AttestationMode.Offline && !string.IsNullOrEmpty(options.RekorUrl))
{
try
{
using var httpClient = CreateHttpClient(options);
var response = await httpClient.GetAsync($"{options.RekorUrl.TrimEnd('/')}/api/v1/log", ct);
if (response.IsSuccessStatusCode && response.Headers.Date.HasValue)
{
referenceTime = response.Headers.Date.Value;
skew = localTime - referenceTime.Value;
referenceSource = "Rekor server";
}
}
catch
{
// Rekor unavailable, try alternative methods
}
}
// Fall back to well-known time endpoint if Rekor failed
if (skew is null)
{
try
{
using var httpClient = CreateHttpClient(options);
var response = await httpClient.GetAsync("https://www.google.com/", ct);
if (response.Headers.Date.HasValue)
{
referenceTime = response.Headers.Date.Value;
skew = localTime - referenceTime.Value;
referenceSource = "HTTP Date header (google.com)";
}
}
catch
{
// Network unavailable
}
}
// If we couldn't get a reference time, check against a reasonable expectation
if (skew is null)
{
// In offline mode or network failure, we can only warn that we couldn't verify
return result
.Info("Clock skew could not be verified (no reference time source available)")
.WithEvidence("Time check", e => e
.Add("LocalTime", localTime.ToString("O"))
.Add("ReferenceSource", "(none)")
.Add("Mode", options.Mode.ToString())
.Add("Note", "Clock skew verification skipped - no network reference available"))
.WithRemediation(r => r
.AddShellStep(1, "Check system time", GetTimeCheckCommand())
.AddManualStep(2, "Configure NTP", "Ensure NTP is configured for time synchronization"))
.Build();
}
var skewSeconds = Math.Abs(skew.Value.TotalSeconds);
// Evaluate against thresholds
if (skewSeconds > options.ClockSkewFailThresholdSeconds)
{
return result
.Fail($"System clock is off by {skewSeconds:F1} seconds (threshold: {options.ClockSkewFailThresholdSeconds}s)")
.WithEvidence("Time comparison", e => e
.Add("LocalTime", localTime.ToString("O"))
.Add("ReferenceTime", referenceTime!.Value.ToString("O"))
.Add("ReferenceSource", referenceSource!)
.Add("SkewSeconds", skewSeconds.ToString("F1"))
.Add("WarnThreshold", options.ClockSkewWarnThresholdSeconds.ToString())
.Add("FailThreshold", options.ClockSkewFailThresholdSeconds.ToString()))
.WithCauses(
"System clock is not synchronized",
"NTP service is not running",
"NTP server is unreachable",
"Hardware clock is misconfigured")
.WithRemediation(r => r
.AddShellStep(1, "Check current time", GetTimeCheckCommand())
.AddShellStep(2, "Force NTP sync", GetNtpSyncCommand())
.AddManualStep(3, "Configure NTP", "Ensure NTP is properly configured and the NTP service is running"))
.WithVerification($"stella doctor --check {CheckId}")
.Build();
}
if (skewSeconds > options.ClockSkewWarnThresholdSeconds)
{
return result
.Warn($"System clock is off by {skewSeconds:F1} seconds (threshold: {options.ClockSkewWarnThresholdSeconds}s)")
.WithEvidence("Time comparison", e => e
.Add("LocalTime", localTime.ToString("O"))
.Add("ReferenceTime", referenceTime!.Value.ToString("O"))
.Add("ReferenceSource", referenceSource!)
.Add("SkewSeconds", skewSeconds.ToString("F1"))
.Add("WarnThreshold", options.ClockSkewWarnThresholdSeconds.ToString())
.Add("FailThreshold", options.ClockSkewFailThresholdSeconds.ToString()))
.WithCauses(
"NTP synchronization drift",
"Infrequent NTP sync interval")
.WithRemediation(r => r
.AddShellStep(1, "Check NTP status", GetNtpStatusCommand())
.AddShellStep(2, "Force NTP sync", GetNtpSyncCommand()))
.WithVerification($"stella doctor --check {CheckId}")
.Build();
}
return result
.Pass($"System clock synchronized (skew: {skewSeconds:F1}s)")
.WithEvidence("Time comparison", e => e
.Add("LocalTime", localTime.ToString("O"))
.Add("ReferenceTime", referenceTime!.Value.ToString("O"))
.Add("ReferenceSource", referenceSource!)
.Add("SkewSeconds", skewSeconds.ToString("F1"))
.Add("WarnThreshold", options.ClockSkewWarnThresholdSeconds.ToString()))
.Build();
}
private static string GetTimeCheckCommand()
{
return OperatingSystem.IsWindows()
? "w32tm /query /status"
: "timedatectl status";
}
private static string GetNtpSyncCommand()
{
return OperatingSystem.IsWindows()
? "w32tm /resync"
: "sudo systemctl restart systemd-timesyncd || sudo ntpdate -u pool.ntp.org";
}
private static string GetNtpStatusCommand()
{
return OperatingSystem.IsWindows()
? "w32tm /query /peers"
: "timedatectl timesync-status || ntpq -p";
}
}