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; /// /// Verifies system clock is within acceptable range for signature verification. /// public sealed class ClockSkewCheck : AttestationCheckBase { /// public override string CheckId => "check.attestation.clock.skew"; /// public override string Name => "Clock Skew Sanity"; /// public override string Description => "Verifies system clock is synchronized within acceptable range for signature verification"; /// public override DoctorSeverity DefaultSeverity => DoctorSeverity.Warn; /// public override IReadOnlyList Tags => ["quick", "attestation", "security", "time"]; /// public override TimeSpan EstimatedDuration => TimeSpan.FromSeconds(3); /// protected override async Task 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"; } }