182 lines
7.5 KiB
C#
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";
|
|
}
|
|
}
|