Staleness & Time Anchor Contract v1.0.0

Status: APPROVED Version: 1.0.0 Effective: 2025-12-19 Owner: AirGap Guild + Findings Ledger Guild Sprint: SPRINT_0510_0001_0001 (unblocks LEDGER-AIRGAP-56-002, LEDGER-AIRGAP-57-001)


1. Purpose

This contract defines how air-gapped StellaOps installations maintain trusted time references, calculate data staleness, and enforce freshness policies. It enables deterministic vulnerability triage even when disconnected from external time sources.

2. Schema References

SchemaLocation
Time Anchordocs/modules/airgap/schemas/time-anchor.schema.json
Ledger Stalenessdocs/modules/airgap/schemas/ledger-airgap-staleness.schema.json
Sealed Modedocs/modules/airgap/schemas/sealed-mode.schema.json

3. Architecture

┌─────────────────────────────────────────────────────────────────────────┐
│                         Air-Gapped Environment                          │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  ┌──────────────┐    ┌──────────────┐    ┌──────────────────────┐      │
│  │ Mirror       │───▶│ AirGap       │───▶│ AirGap Time          │      │
│  │ Bundle       │    │ Controller   │    │ Service              │      │
│  │ (time anchor)│    └──────────────┘    └──────────────────────┘      │
│  └──────────────┘            │                     │                    │
│                              │                     │                    │
│                              ▼                     ▼                    │
│                    ┌──────────────────────────────────────────┐        │
│                    │         Staleness Calculator              │        │
│                    │         (drift, budgets, validation)      │        │
│                    └──────────────────────────────────────────┘        │
│                              │                     │                    │
│                ┌─────────────┴─────────────────────┴───────────┐       │
│                │                                               │       │
│                ▼                                               ▼       │
│  ┌──────────────────────┐                    ┌──────────────────────┐  │
│  │ Findings Ledger      │                    │ Policy Engine        │  │
│  │ (staleness tracking) │                    │ (evaluation gating)  │  │
│  └──────────────────────┘                    └──────────────────────┘  │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

4. Core Types

4.1 TimeAnchor

A cryptographically signed time reference:

public sealed record TimeAnchor
{
    /// <summary>RFC 3339 timestamp of the anchor.</summary>
    public required DateTimeOffset AnchorTime { get; init; }

    /// <summary>Source of the time anchor.</summary>
    public required TimeSource Source { get; init; }

    /// <summary>Format identifier (roughtime-v1, rfc3161-v1).</summary>
    public required string Format { get; init; }

    /// <summary>SHA-256 digest of the time token.</summary>
    public required string TokenDigest { get; init; }

    /// <summary>Signing key fingerprint.</summary>
    public string? SignatureFingerprint { get; init; }

    /// <summary>Verification status.</summary>
    public VerificationStatus? Verification { get; init; }

    /// <summary>Monotonic counter for replay protection.</summary>
    public long? MonotonicCounter { get; init; }
}

public enum TimeSource
{
    Roughtime = 0,
    Rfc3161 = 1,
    HardwareClock = 2,
    AttestationTsa = 3,
    Manual = 4,
    Unknown = 5
}

public sealed record VerificationStatus
{
    public required VerificationState Status { get; init; }
    public string? Reason { get; init; }
    public DateTimeOffset? VerifiedAt { get; init; }
}

public enum VerificationState
{
    Unknown = 0,
    Passed = 1,
    Failed = 2
}

4.2 StalenessBudget

Configuration for acceptable data freshness:

public sealed record StalenessBudget
{
    /// <summary>Budget identifier.</summary>
    public required string BudgetId { get; init; }

    /// <summary>Domain this budget applies to.</summary>
    public required string DomainId { get; init; }

    /// <summary>Maximum staleness in seconds before data is stale.</summary>
    public required TimeSpan FreshnessThreshold { get; init; }

    /// <summary>Warning threshold (percentage of freshness threshold).</summary>
    public decimal WarningThresholdPercent { get; init; } = 75m;

    /// <summary>Critical threshold (percentage of freshness threshold).</summary>
    public decimal CriticalThresholdPercent { get; init; } = 90m;

    /// <summary>Grace period after threshold before hard enforcement.</summary>
    public TimeSpan GracePeriod { get; init; } = TimeSpan.FromDays(1);

    /// <summary>Enforcement mode.</summary>
    public EnforcementMode EnforcementMode { get; init; } = EnforcementMode.Strict;
}

public enum EnforcementMode
{
    Strict = 0,   // Block operations when stale
    Warn = 1,     // Allow but log warnings
    Disabled = 2  // No enforcement
}

4.3 StalenessEvaluation

Result of staleness calculation:

public sealed record StalenessEvaluation
{
    /// <summary>Domain evaluated.</summary>
    public required string DomainId { get; init; }

    /// <summary>Current staleness duration.</summary>
    public required TimeSpan CurrentStaleness { get; init; }

    /// <summary>Configured threshold.</summary>
    public required TimeSpan Threshold { get; init; }

    /// <summary>Staleness as percentage of threshold.</summary>
    public required decimal PercentOfThreshold { get; init; }

    /// <summary>Overall status.</summary>
    public required StalenessStatus Status { get; init; }

    /// <summary>When data will become stale.</summary>
    public DateTimeOffset? ProjectedStaleAt { get; init; }

    /// <summary>Time anchor used for calculation.</summary>
    public required TimeAnchor TimeAnchor { get; init; }

    /// <summary>Last bundle import timestamp.</summary>
    public required DateTimeOffset LastImportAt { get; init; }

    /// <summary>Source timestamp of last bundle.</summary>
    public required DateTimeOffset LastSourceTimestamp { get; init; }
}

public enum StalenessStatus
{
    Fresh = 0,      // < warning threshold
    Warning = 1,    // >= warning, < critical
    Critical = 2,   // >= critical, < threshold
    Stale = 3,      // >= threshold, < threshold + grace
    Breached = 4    // >= threshold + grace
}

4.4 BundleProvenance

Provenance record for imported bundles:

public sealed record BundleProvenance
{
    /// <summary>Unique bundle identifier.</summary>
    public required Guid BundleId { get; init; }

    /// <summary>Bundle domain (vex-advisories, vulnerability-feeds, etc.).</summary>
    public required string DomainId { get; init; }

    /// <summary>When bundle was imported.</summary>
    public required DateTimeOffset ImportedAt { get; init; }

    /// <summary>Original generation timestamp from source.</summary>
    public required DateTimeOffset SourceTimestamp { get; init; }

    /// <summary>Source environment identifier.</summary>
    public string? SourceEnvironment { get; init; }

    /// <summary>SHA-256 digest of bundle contents.</summary>
    public required string BundleDigest { get; init; }

    /// <summary>SHA-256 digest of bundle manifest.</summary>
    public string? ManifestDigest { get; init; }

    /// <summary>Staleness at import time.</summary>
    public required TimeSpan StalenessAtImport { get; init; }

    /// <summary>Time anchor used for staleness calculation.</summary>
    public required TimeAnchor TimeAnchor { get; init; }

    /// <summary>DSSE attestation covering this bundle.</summary>
    public BundleAttestation? Attestation { get; init; }

    /// <summary>Exports included in this bundle.</summary>
    public ImmutableArray<ExportRecord> Exports { get; init; }
}

5. Staleness Domains

Domain IDDescriptionDefault ThresholdDefault Grace
vulnerability-feedsAdvisory and CVE data7 days1 day
vex-advisoriesVEX statements7 days1 day
scanner-signaturesScanner detection rules14 days3 days
policy-packsPolicy bundles30 days7 days
trust-rootsCertificate/key roots90 days14 days
runtime-evidenceRuntime observation data1 day4 hours

6. Time Anchor Verification

6.1 Roughtime Verification

public interface IRoughtimeVerifier
{
    /// <summary>
    /// Verifies a Roughtime response against trusted servers.
    /// </summary>
    Task<TimeAnchorValidationResult> VerifyAsync(
        byte[] roughtimeResponse,
        RoughtimeRoot[] trustedRoots,
        CancellationToken cancellationToken = default);
}

Roughtime provides:

6.2 RFC 3161 Verification

public interface IRfc3161Verifier
{
    /// <summary>
    /// Verifies an RFC 3161 timestamp token.
    /// </summary>
    Task<TimeAnchorValidationResult> VerifyAsync(
        byte[] timestampToken,
        Rfc3161Root[] trustedRoots,
        CancellationToken cancellationToken = default);
}

RFC 3161 provides:

6.3 Validation Result

public sealed record TimeAnchorValidationResult
{
    public required bool IsValid { get; init; }
    public required TimeAnchor? Anchor { get; init; }
    public TimeAnchorError? Error { get; init; }
    public TimeSpan? Uncertainty { get; init; }
}

public enum TimeAnchorError
{
    None = 0,
    SignatureInvalid = 1,
    RootNotTrusted = 2,
    TokenExpired = 3,
    TokenMalformed = 4,
    CounterReplay = 5,
    UncertaintyTooHigh = 6
}

7. API Endpoints

7.1 AirGap Time Service

EndpointMethodDescription
GET /api/v1/time/statusGETCurrent anchor metadata and drift
GET /api/v1/time/anchorGETActive time anchor
POST /api/v1/time/anchorPOSTImport new time anchor
GET /api/v1/time/metricsGETPrometheus metrics
GET /api/v1/time/healthGETHealth check

7.2 Staleness Endpoints

EndpointMethodDescription
GET /api/v1/staleness/domainsGETList all domain staleness
GET /api/v1/staleness/domains/{domainId}GETGet domain staleness
POST /api/v1/staleness/validatePOSTValidate staleness for context
GET /api/v1/staleness/configGETGet staleness configuration
PUT /api/v1/staleness/configPUTUpdate staleness configuration

7.3 Response Formats

{
  "domainId": "vex-advisories",
  "currentStaleness": "PT172800S",
  "threshold": "PT604800S",
  "percentOfThreshold": 28.57,
  "status": "fresh",
  "projectedStaleAt": "2025-12-26T10:00:00Z",
  "timeAnchor": {
    "anchorTime": "2025-12-19T10:00:00Z",
    "source": "roughtime",
    "format": "roughtime-v1",
    "tokenDigest": "sha256:abc123...",
    "verification": {
      "status": "passed",
      "verifiedAt": "2025-12-19T10:00:01Z"
    }
  },
  "lastImportAt": "2025-12-17T10:00:00Z",
  "lastSourceTimestamp": "2025-12-17T08:00:00Z"
}

8. Integration Points

8.1 Findings Ledger Integration

The Ledger tracks staleness per projection:

public interface IStalenessValidationService
{
    /// <summary>
    /// Validates that data is fresh enough for the given context.
    /// </summary>
    Task<StalenessValidationResult> ValidateAsync(
        string tenantId,
        string domainId,
        StalenessContext context,
        CancellationToken cancellationToken = default);

    /// <summary>
    /// Updates staleness tracking after bundle import.
    /// </summary>
    Task UpdateStalenessAsync(
        string tenantId,
        BundleProvenance provenance,
        CancellationToken cancellationToken = default);
}

public enum StalenessContext
{
    Export = 0,          // Generating exports
    Query = 1,           // Querying data
    PolicyEval = 2,      // Policy evaluation
    Attestation = 3      // Creating attestations
}

8.2 Policy Engine Integration

Policy Engine gates evaluations based on staleness:

public interface ISealedModeService
{
    /// <summary>
    /// Checks if sealed mode should block the operation.
    /// </summary>
    Task<SealedModeDecision> CheckAsync(
        string tenantId,
        SealedModeContext context,
        CancellationToken cancellationToken = default);
}

public sealed record SealedModeDecision
{
    public required bool IsBlocked { get; init; }
    public SealedModeReason? Reason { get; init; }
    public ImmutableArray<StalenessEvaluation> StaleDomains { get; init; }
}

public enum SealedModeReason
{
    None = 0,
    DataStale = 1,
    TimeAnchorMissing = 2,
    TimeAnchorExpired = 3,
    SignatureInvalid = 4
}

9. Telemetry

9.1 Metrics

MetricTypeLabelsDescription
airgap_anchor_age_secondsgauge-Age of current time anchor
airgap_anchor_drift_secondsgauge-Drift from anchor time
airgap_anchor_expiry_secondsgauge-Seconds until anchor expires
airgap_staleness_secondsgaugedomainCurrent staleness per domain
airgap_staleness_threshold_secondsgaugedomainThreshold per domain
airgap_staleness_percentgaugedomainStaleness as % of threshold
airgap_staleness_statusgaugedomain, statusCurrent status (0=fresh, 3=stale)
airgap_bundle_imports_totalcounterdomain, resultBundle imports
airgap_validation_totalcounterdomain, context, resultStaleness validations

9.2 Alerts

# Recommended alerting rules
groups:
  - name: airgap-staleness
    rules:
      - alert: AirGapDataApproachingStale
        expr: airgap_staleness_percent > 75
        for: 1h
        labels:
          severity: warning
        annotations:
          summary: "{{ $labels.domain }} data approaching staleness"

      - alert: AirGapDataStale
        expr: airgap_staleness_percent >= 100
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "{{ $labels.domain }} data is stale"

      - alert: AirGapTimeAnchorMissing
        expr: airgap_anchor_age_seconds > 86400
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "Time anchor is older than 24 hours"

10. Configuration

# etc/airgap.yaml
AirGap:
  Time:
    Enabled: true
    TrustRootsPath: "/etc/stellaops/trust-roots.json"
    MaxAnchorAgeHours: 168  # 7 days
    MaxUncertaintyMs: 5000  # 5 seconds

  Staleness:
    DefaultThresholdDays: 7
    DefaultGracePeriodDays: 1
    EnforcementMode: "Strict"  # Strict, Warn, Disabled

    Domains:
      vulnerability-feeds:
        ThresholdDays: 7
        GracePeriodDays: 1
      vex-advisories:
        ThresholdDays: 7
        GracePeriodDays: 1
      runtime-evidence:
        ThresholdDays: 1
        GracePeriodHours: 4

    Notifications:
      - PercentOfThreshold: 75
        Severity: warning
        Channels: [slack, metric]
      - PercentOfThreshold: 90
        Severity: critical
        Channels: [email, slack, metric]

11. Error Codes

CodeDescriptionResolution
ERR_AIRGAP_STALEData exceeds staleness thresholdImport fresh bundle
ERR_AIRGAP_NO_BUNDLENo bundle imported for domainImport initial bundle
ERR_AIRGAP_TIME_ANCHOR_MISSINGNo time anchor availableImport time anchor with bundle
ERR_AIRGAP_TIME_DRIFTExcessive drift detectedRe-verify time anchor
ERR_AIRGAP_ATTESTATION_INVALIDBundle attestation invalidVerify bundle source
ERR_AIRGAP_SIGNATURE_INVALIDTime token signature invalidCheck trust roots
ERR_AIRGAP_COUNTER_REPLAYMonotonic counter replayImport newer anchor

Changelog

VersionDateChanges
1.0.02025-12-19Initial release